diff options
757 files changed, 21025 insertions, 2068 deletions
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/.rubocop.yml b/.rubocop.yml index ea4d365761e..05b8ecc3b00 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -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 247eb1e3643..492e4b9aebf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,17 +1,68 @@ 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) + - 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 + +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) @@ -29,21 +80,42 @@ v 8.0.0 (unreleased) - 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) - - Refactored service API and added automatically service docs generator (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 + - 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 + - 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 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 @@ -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,29 +18,29 @@ 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 'omniauth_crowd' -gem 'doorkeeper', '2.1.3' 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' @@ -44,7 +52,7 @@ gem "gitlab_git", '~> 7.2.15' # 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' @@ -59,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 @@ -107,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' @@ -157,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' @@ -173,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' @@ -245,30 +260,57 @@ 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 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 "mail_room", "~> 0.5.2" + +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' -gem 'octokit', '3.7.0' +# Scheduled +gem 'whenever', '~> 0.8.4', require: false -gem "mail_room", "~> 0.4.2" +# OAuth +gem 'oauth2', '~> 1.0.0' -gem 'email_reply_parser' +# 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 2df318f3382..4386c6b9abb 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,36 @@ 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) 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 +86,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 +94,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 +104,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 +119,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 +129,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 +169,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 +208,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 +222,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 +233,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 +248,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,7 +276,7 @@ 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) activesupport (~> 4.0) @@ -285,16 +289,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 +311,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 +337,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 +362,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.2) + 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,30 +430,30 @@ 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) @@ -445,7 +469,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) @@ -453,60 +479,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) @@ -521,10 +546,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) @@ -535,32 +560,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) @@ -575,16 +603,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) @@ -608,7 +638,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) @@ -627,19 +660,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) @@ -670,31 +704,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 @@ -704,35 +739,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 @@ -744,145 +783,157 @@ 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) + 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_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.2) - 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 + omniauth-shibboleth (~> 1.1.1) + omniauth-twitter (~> 1.0.1) omniauth_crowd - org-ruby (= 0.9.12) - pg + 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.5 @@ -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..99d5bc0b6ca 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [](https://ci.gitlab.com/projects/1?ref=master) [](https://semaphoreci.com/gitlabhq/gitlabhq) [](https://codeclimate.com/github/gitlabhq/gitlabhq) -[](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master) +[](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master) ## Canonical source @@ -1 +1 @@ -8.0.0.pre +8.1.0.pre diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf b/app/assets/fonts/SourceSansPro-Bold.ttf Binary files differindex 50d81bdad58..5d65c93242f 100755 --- a/app/assets/fonts/SourceSansPro-Bold.ttf +++ b/app/assets/fonts/SourceSansPro-Bold.ttf diff --git a/app/assets/fonts/SourceSansPro-Light.ttf b/app/assets/fonts/SourceSansPro-Light.ttf Binary files differindex 5f64679f6b9..83a0a336661 100755 --- a/app/assets/fonts/SourceSansPro-Light.ttf +++ b/app/assets/fonts/SourceSansPro-Light.ttf diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf b/app/assets/fonts/SourceSansPro-Regular.ttf Binary files differindex 91e9ea5757f..44486cdc670 100755 --- a/app/assets/fonts/SourceSansPro-Regular.ttf +++ b/app/assets/fonts/SourceSansPro-Regular.ttf diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf b/app/assets/fonts/SourceSansPro-Semibold.ttf Binary files differindex 5020594826b..86b00c067e0 100755 --- a/app/assets/fonts/SourceSansPro-Semibold.ttf +++ b/app/assets/fonts/SourceSansPro-Semibold.ttf diff --git a/app/assets/images/ci/arch.jpg b/app/assets/images/ci/arch.jpg Binary files differnew file mode 100644 index 00000000000..0e05674e840 --- /dev/null +++ b/app/assets/images/ci/arch.jpg diff --git a/app/assets/images/ci/favicon.ico b/app/assets/images/ci/favicon.ico Binary files differnew file mode 100644 index 00000000000..9663d4d00b9 --- /dev/null +++ b/app/assets/images/ci/favicon.ico diff --git a/app/assets/images/ci/loader.gif b/app/assets/images/ci/loader.gif Binary files differnew file mode 100644 index 00000000000..2fcb8f2da0d --- /dev/null +++ b/app/assets/images/ci/loader.gif diff --git a/app/assets/images/ci/no_avatar.png b/app/assets/images/ci/no_avatar.png Binary files differnew file mode 100644 index 00000000000..752d26adba7 --- /dev/null +++ b/app/assets/images/ci/no_avatar.png diff --git a/app/assets/images/ci/rails.png b/app/assets/images/ci/rails.png Binary files differnew file mode 100644 index 00000000000..d5edc04e65f --- /dev/null +++ b/app/assets/images/ci/rails.png diff --git a/app/assets/images/ci/service_sample.png b/app/assets/images/ci/service_sample.png Binary files differnew file mode 100644 index 00000000000..65d29e3fd89 --- /dev/null +++ b/app/assets/images/ci/service_sample.png 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 = $("<div/>").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);d<b||d>c;)a=d<b?a/2:2*a,d=Math.round(e/a);c=[];z(f,c,d,h,a);return{steps:d,stepValue:a,graphMin:h,labels:c}}function z(a,c,b,e,h){if(a)for(var f=1;f<b+1;f++)c.push(E(a,{value:(e+h*f).toFixed(0!=h%1?h.toString().split(".")[1].length:0)}))}function A(a,c,b){return!isNaN(parseFloat(c))&&isFinite(c)&&a>c?c:!isNaN(parseFloat(b))&& +isFinite(b)&&a<b?b:a}function y(a,c){var b={},e;for(e in a)b[e]=a[e];for(e in c)b[e]=c[e];return b}function E(a,c){var b=!/\W/.test(a)?F[a]=F[a]||E(document.getElementById(a).innerHTML):new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+a.replace(/[\r\t\n]/g," ").split("<%").join("\t").replace(/((^|%>)[^\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<Math.abs(1)?(e=1,c=b/4):c=b/(2* +Math.PI)*Math.asin(1/e);return-(e*Math.pow(2,10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b))},easeOutElastic: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<Math.abs(1)?(e=1,c=b/4):c=b/(2*Math.PI)*Math.asin(1/e);return e*Math.pow(2,-10*a)*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInOutElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(2==(a/=0.5))return 1;b||(b=1*0.3*1.5);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*Math.PI)*Math.asin(1/e);return 1>a?-0.5*e*Math.pow(2,10* +(a-=1))*Math.sin((1*a-c)*2*Math.PI/b):0.5*e*Math.pow(2,-10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInBack:function(a){return 1*(a/=1)*a*(2.70158*a-1.70158)},easeOutBack:function(a){return 1*((a=a/1-1)*a*(2.70158*a+1.70158)+1)},easeInOutBack:function(a){var c=1.70158;return 1>(a/=0.5)?0.5*a*a*(((c*=1.525)+1)*a-c):0.5*((a-=2)*a*(((c*=1.525)+1)*a+c)+2)},easeInBounce:function(a){return 1-B.easeOutBounce(1-a)},easeOutBounce:function(a){return(a/=1)<1/2.75?1*7.5625*a*a:a<2/2.75?1*(7.5625*(a-=1.5/2.75)* +a+0.75):a<2.5/2.75?1*(7.5625*(a-=2.25/2.75)*a+0.9375):1*(7.5625*(a-=2.625/2.75)*a+0.984375)},easeInOutBounce:function(a){return 0.5>a?0.5*B.easeInBounce(2*a):0.5*B.easeOutBounce(2*a-1)+0.5}},q=s.canvas.width,u=s.canvas.height;window.devicePixelRatio&&(s.canvas.style.width=q+"px",s.canvas.style.height=u+"px",s.canvas.height=u*window.devicePixelRatio,s.canvas.width=q*window.devicePixelRatio,s.scale(window.devicePixelRatio,window.devicePixelRatio));this.PolarArea=function(a,c){r.PolarArea.defaults={scaleOverlay:!0, +scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce", +animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.PolarArea.defaults,c):r.PolarArea.defaults;return new G(a,b,s)};this.Radar=function(a,c){r.Radar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!1,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)", +scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,angleShowLineOut:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:12,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Radar.defaults,c):r.Radar.defaults;return new H(a,b,s)};this.Pie=function(a, +c){r.Pie.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.Pie.defaults,c):r.Pie.defaults;return new I(a,b,s)};this.Doughnut=function(a,c){r.Doughnut.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1, +onAnimationComplete:null};var b=c?y(r.Doughnut.defaults,c):r.Doughnut.defaults;return new J(a,b,s)};this.Line=function(a,c){r.Line.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0, +pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:2,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Line.defaults,c):r.Line.defaults;return new K(a,b,s)};this.Bar=function(a,c){r.Bar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'", +scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Bar.defaults,c):r.Bar.defaults;return new L(a,b,s)};var G=function(a,c,b){var e,h,f,d,g,k,j,l,m;g=Math.min.apply(Math,[q,u])/2;g-=Math.max.apply(Math,[0.5*c.scaleFontSize,0.5*c.scaleLineWidth]); +d=2*c.scaleFontSize;c.scaleShowLabelBackdrop&&(d+=2*c.scaleBackdropPaddingY,g-=1.5*c.scaleBackdropPaddingY);l=g;d=d?d:5;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.length;f++)a[f].value>e&&(e=a[f].value),a[f].value<h&&(h=a[f].value);f=Math.floor(l/(0.66*d));d=Math.floor(0.5*(l/d));m=c.scaleShowLabels?c.scaleLabel:null;c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(m,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(l,f,d,e,h, +m);k=g/j.steps;x(c,function(){for(var a=0;a<j.steps;a++)if(c.scaleShowLine&&(b.beginPath(),b.arc(q/2,u/2,k*(a+1),0,2*Math.PI,!0),b.strokeStyle=c.scaleLineColor,b.lineWidth=c.scaleLineWidth,b.stroke()),c.scaleShowLabels){b.textAlign="center";b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;var e=j.labels[a];if(c.scaleShowLabelBackdrop){var d=b.measureText(e).width;b.fillStyle=c.scaleBackdropColor;b.beginPath();b.rect(Math.round(q/2-d/2-c.scaleBackdropPaddingX),Math.round(u/2-k*(a+ +1)-0.5*c.scaleFontSize-c.scaleBackdropPaddingY),Math.round(d+2*c.scaleBackdropPaddingX),Math.round(c.scaleFontSize+2*c.scaleBackdropPaddingY));b.fill()}b.textBaseline="middle";b.fillStyle=c.scaleFontColor;b.fillText(e,q/2,u/2-k*(a+1))}},function(e){var d=-Math.PI/2,g=2*Math.PI/a.length,f=1,h=1;c.animation&&(c.animateScale&&(f=e),c.animateRotate&&(h=e));for(e=0;e<a.length;e++)b.beginPath(),b.arc(q/2,u/2,f*v(a[e].value,j,k),d,d+h*g,!1),b.lineTo(q/2,u/2),b.closePath(),b.fillStyle=a[e].color,b.fill(), +c.segmentShowStroke&&(b.strokeStyle=c.segmentStrokeColor,b.lineWidth=c.segmentStrokeWidth,b.stroke()),d+=h*g},b)},H=function(a,c,b){var e,h,f,d,g,k,j,l,m;a.labels||(a.labels=[]);g=Math.min.apply(Math,[q,u])/2;d=2*c.scaleFontSize;for(e=l=0;e<a.labels.length;e++)b.font=c.pointLabelFontStyle+" "+c.pointLabelFontSize+"px "+c.pointLabelFontFamily,h=b.measureText(a.labels[e]).width,h>l&&(l=h);g-=Math.max.apply(Math,[l,1.5*(c.pointLabelFontSize/2)]);g-=c.pointLabelFontSize;l=g=A(g,null,0);d=d?d:5;e=Number.MIN_VALUE; +h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(m=0;m<a.datasets[f].data.length;m++)a.datasets[f].data[m]>e&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]<h&&(h=a.datasets[f].data[m]);f=Math.floor(l/(0.66*d));d=Math.floor(0.5*(l/d));m=c.scaleShowLabels?c.scaleLabel:null;c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(m,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(l,f,d,e,h,m);k=g/j.steps;x(c,function(){var e=2*Math.PI/ +a.datasets[0].data.length;b.save();b.translate(q/2,u/2);if(c.angleShowLineOut){b.strokeStyle=c.angleLineColor;b.lineWidth=c.angleLineWidth;for(var d=0;d<a.datasets[0].data.length;d++)b.rotate(e),b.beginPath(),b.moveTo(0,0),b.lineTo(0,-g),b.stroke()}for(d=0;d<j.steps;d++){b.beginPath();if(c.scaleShowLine){b.strokeStyle=c.scaleLineColor;b.lineWidth=c.scaleLineWidth;b.moveTo(0,-k*(d+1));for(var f=0;f<a.datasets[0].data.length;f++)b.rotate(e),b.lineTo(0,-k*(d+1));b.closePath();b.stroke()}c.scaleShowLabels&& +(b.textAlign="center",b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily,b.textBaseline="middle",c.scaleShowLabelBackdrop&&(f=b.measureText(j.labels[d]).width,b.fillStyle=c.scaleBackdropColor,b.beginPath(),b.rect(Math.round(-f/2-c.scaleBackdropPaddingX),Math.round(-k*(d+1)-0.5*c.scaleFontSize-c.scaleBackdropPaddingY),Math.round(f+2*c.scaleBackdropPaddingX),Math.round(c.scaleFontSize+2*c.scaleBackdropPaddingY)),b.fill()),b.fillStyle=c.scaleFontColor,b.fillText(j.labels[d],0,-k*(d+ +1)))}for(d=0;d<a.labels.length;d++){b.font=c.pointLabelFontStyle+" "+c.pointLabelFontSize+"px "+c.pointLabelFontFamily;b.fillStyle=c.pointLabelFontColor;var f=Math.sin(e*d)*(g+c.pointLabelFontSize),h=Math.cos(e*d)*(g+c.pointLabelFontSize);b.textAlign=e*d==Math.PI||0==e*d?"center":e*d>Math.PI?"right":"left";b.textBaseline="middle";b.fillText(a.labels[d],f,-h)}b.restore()},function(d){var e=2*Math.PI/a.datasets[0].data.length;b.save();b.translate(q/2,u/2);for(var g=0;g<a.datasets.length;g++){b.beginPath(); +b.moveTo(0,d*-1*v(a.datasets[g].data[0],j,k));for(var f=1;f<a.datasets[g].data.length;f++)b.rotate(e),b.lineTo(0,d*-1*v(a.datasets[g].data[f],j,k));b.closePath();b.fillStyle=a.datasets[g].fillColor;b.strokeStyle=a.datasets[g].strokeColor;b.lineWidth=c.datasetStrokeWidth;b.fill();b.stroke();if(c.pointDot){b.fillStyle=a.datasets[g].pointColor;b.strokeStyle=a.datasets[g].pointStrokeColor;b.lineWidth=c.pointDotStrokeWidth;for(f=0;f<a.datasets[g].data.length;f++)b.rotate(e),b.beginPath(),b.arc(0,d*-1* +v(a.datasets[g].data[f],j,k),c.pointDotRadius,2*Math.PI,!1),b.fill(),b.stroke()}b.rotate(e)}b.restore()},b)},I=function(a,c,b){for(var e=0,h=Math.min.apply(Math,[u/2,q/2])-5,f=0;f<a.length;f++)e+=a[f].value;x(c,null,function(d){var g=-Math.PI/2,f=1,j=1;c.animation&&(c.animateScale&&(f=d),c.animateRotate&&(j=d));for(d=0;d<a.length;d++){var l=j*a[d].value/e*2*Math.PI;b.beginPath();b.arc(q/2,u/2,f*h,g,g+l);b.lineTo(q/2,u/2);b.closePath();b.fillStyle=a[d].color;b.fill();c.segmentShowStroke&&(b.lineWidth= +c.segmentStrokeWidth,b.strokeStyle=c.segmentStrokeColor,b.stroke());g+=l}},b)},J=function(a,c,b){for(var e=0,h=Math.min.apply(Math,[u/2,q/2])-5,f=h*(c.percentageInnerCutout/100),d=0;d<a.length;d++)e+=a[d].value;x(c,null,function(d){var k=-Math.PI/2,j=1,l=1;c.animation&&(c.animateScale&&(j=d),c.animateRotate&&(l=d));for(d=0;d<a.length;d++){var m=l*a[d].value/e*2*Math.PI;b.beginPath();b.arc(q/2,u/2,j*h,k,k+m,!1);b.arc(q/2,u/2,j*f,k+m,k,!0);b.closePath();b.fillStyle=a[d].color;b.fill();c.segmentShowStroke&& +(b.lineWidth=c.segmentStrokeWidth,b.strokeStyle=c.segmentStrokeColor,b.stroke());k+=m}},b)},K=function(a,c,b){var e,h,f,d,g,k,j,l,m,t,r,n,p,s=0;g=u;b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;t=1;for(d=0;d<a.labels.length;d++)e=b.measureText(a.labels[d]).width,t=e>t?e:t;q/a.labels.length<t?(s=45,q/a.labels.length<Math.cos(s)*t?(s=90,g-=t):g-=Math.sin(s)*t):g-=c.scaleFontSize;d=c.scaleFontSize;g=g-5-d;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(l= +0;l<a.datasets[f].data.length;l++)a.datasets[f].data[l]>e&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]<h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily; +for(e=0;e<j.labels.length;e++)h=b.measureText(j.labels[e]).width,d=h>d?h:d;d+=10}r=q-d-t;m=Math.floor(r/(a.labels.length-1));n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0<s?(b.save(),b.textAlign="right"):b.textAlign="center";b.fillStyle=c.scaleFontColor;for(var d=0;d<a.labels.length;d++)b.save(),0<s?(b.translate(n+d*m,p+c.scaleFontSize),b.rotate(-(s*(Math.PI/180))),b.fillText(a.labels[d], +0,0),b.restore()):b.fillText(a.labels[d],n+d*m,p+c.scaleFontSize+3),b.beginPath(),b.moveTo(n+d*m,p+3),c.scaleShowGridLines&&0<d?(b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+d*m,5)):b.lineTo(n+d*m,p+3),b.stroke();b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(n,p+5);b.lineTo(n,5);b.stroke();b.textAlign="right";b.textBaseline="middle";for(d=0;d<j.steps;d++)b.beginPath(),b.moveTo(n-3,p-(d+1)*k),c.scaleShowGridLines?(b.lineWidth=c.scaleGridLineWidth, +b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+r+5,p-(d+1)*k)):b.lineTo(n-0.5,p-(d+1)*k),b.stroke(),c.scaleShowLabels&&b.fillText(j.labels[d],n-8,p-(d+1)*k)},function(d){function e(b,c){return p-d*v(a.datasets[b].data[c],j,k)}for(var f=0;f<a.datasets.length;f++){b.strokeStyle=a.datasets[f].strokeColor;b.lineWidth=c.datasetStrokeWidth;b.beginPath();b.moveTo(n,p-d*v(a.datasets[f].data[0],j,k));for(var g=1;g<a.datasets[f].data.length;g++)c.bezierCurve?b.bezierCurveTo(n+m*(g-0.5),e(f,g-1),n+m*(g-0.5), +e(f,g),n+m*g,e(f,g)):b.lineTo(n+m*g,e(f,g));b.stroke();c.datasetFill?(b.lineTo(n+m*(a.datasets[f].data.length-1),p),b.lineTo(n,p),b.closePath(),b.fillStyle=a.datasets[f].fillColor,b.fill()):b.closePath();if(c.pointDot){b.fillStyle=a.datasets[f].pointColor;b.strokeStyle=a.datasets[f].pointStrokeColor;b.lineWidth=c.pointDotStrokeWidth;for(g=0;g<a.datasets[f].data.length;g++)b.beginPath(),b.arc(n+m*g,p-d*v(a.datasets[f].data[g],j,k),c.pointDotRadius,0,2*Math.PI,!0),b.fill(),b.stroke()}}},b)},L=function(a, +c,b){var e,h,f,d,g,k,j,l,m,t,r,n,p,s,w=0;g=u;b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;t=1;for(d=0;d<a.labels.length;d++)e=b.measureText(a.labels[d]).width,t=e>t?e:t;q/a.labels.length<t?(w=45,q/a.labels.length<Math.cos(w)*t?(w=90,g-=t):g-=Math.sin(w)*t):g-=c.scaleFontSize;d=c.scaleFontSize;g=g-5-d;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(l=0;l<a.datasets[f].data.length;l++)a.datasets[f].data[l]>e&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]< +h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;for(e=0;e<j.labels.length;e++)h=b.measureText(j.labels[e]).width,d=h>d?h:d;d+=10}r=q-d-t;m= +Math.floor(r/a.labels.length);s=(m-2*c.scaleGridLineWidth-2*c.barValueSpacing-(c.barDatasetSpacing*a.datasets.length-1)-(c.barStrokeWidth/2*a.datasets.length-1))/a.datasets.length;n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0<w?(b.save(),b.textAlign="right"):b.textAlign="center";b.fillStyle=c.scaleFontColor;for(var d=0;d<a.labels.length;d++)b.save(),0<w?(b.translate(n+ +d*m,p+c.scaleFontSize),b.rotate(-(w*(Math.PI/180))),b.fillText(a.labels[d],0,0),b.restore()):b.fillText(a.labels[d],n+d*m+m/2,p+c.scaleFontSize+3),b.beginPath(),b.moveTo(n+(d+1)*m,p+3),b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+(d+1)*m,5),b.stroke();b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(n,p+5);b.lineTo(n,5);b.stroke();b.textAlign="right";b.textBaseline="middle";for(d=0;d<j.steps;d++)b.beginPath(),b.moveTo(n-3,p-(d+1)* +k),c.scaleShowGridLines?(b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+r+5,p-(d+1)*k)):b.lineTo(n-0.5,p-(d+1)*k),b.stroke(),c.scaleShowLabels&&b.fillText(j.labels[d],n-8,p-(d+1)*k)},function(d){b.lineWidth=c.barStrokeWidth;for(var e=0;e<a.datasets.length;e++){b.fillStyle=a.datasets[e].fillColor;b.strokeStyle=a.datasets[e].strokeColor;for(var f=0;f<a.datasets[e].data.length;f++){var g=n+c.barValueSpacing+m*f+s*e+c.barDatasetSpacing*e+c.barStrokeWidth*e;b.beginPath(); +b.moveTo(g,p);b.lineTo(g,p-d*v(a.datasets[e].data[f],j,k)+c.barStrokeWidth/2);b.lineTo(g+s,p-d*v(a.datasets[e].data[f],j,k)+c.barStrokeWidth/2);b.lineTo(g+s,p);c.barShowStroke&&b.stroke();b.closePath();b.fill()}}},b)},D=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){window.setTimeout(a,1E3/60)},F={}};
\ No newline at end of file diff --git a/app/assets/javascripts/ci/application.js.coffee b/app/assets/javascripts/ci/application.js.coffee new file mode 100644 index 00000000000..05aa0f366bb --- /dev/null +++ b/app/assets/javascripts/ci/application.js.coffee @@ -0,0 +1,40 @@ +# This is a manifest file that'll be compiled into application.js, which will include all the files +# listed below. +# +# Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +# or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. +# +# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +# the compiled file. +# +# WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD +# GO AFTER THE REQUIRES BELOW. +# +#= require pager +#= require jquery_nested_form +#= require_tree . +# +$(document).on 'click', '.edit-runner-link', (event) -> + event.preventDefault() + + descr = $(this).closest('.runner-description').first() + descr.addClass('hide') + form = descr.next('.runner-description-form') + descrInput = form.find('input.description') + originalValue = descrInput.val() + form.removeClass('hide') + form.find('.cancel').on 'click', (event) -> + event.preventDefault() + + form.addClass('hide') + descrInput.val(originalValue) + descr.removeClass('hide') + +$(document).on 'click', '.assign-all-runner', -> + $(this).replaceWith('<i class="fa fa-refresh fa-spin"></i> Assign in progress..') + +window.unbindEvents = -> + $(document).unbind('scroll') + $(document).off('scroll') + +document.addEventListener("page:fetch", unbindEvents) diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee new file mode 100644 index 00000000000..c30859b484b --- /dev/null +++ b/app/assets/javascripts/ci/build.coffee @@ -0,0 +1,41 @@ +class CiBuild + @interval: null + + constructor: (build_url, build_status) -> + clearInterval(CiBuild.interval) + + if build_status == "running" || build_status == "pending" + # + # Bind autoscroll button to follow build output + # + $("#autoscroll-button").bind "click", -> + state = $(this).data("state") + if "enabled" is state + $(this).data "state", "disabled" + $(this).text "enable autoscroll" + else + $(this).data "state", "enabled" + $(this).text "disable autoscroll" + + # + # Check for new build output if user still watching build page + # Only valid for runnig build when output changes during time + # + CiBuild.interval = setInterval => + if window.location.href is build_url + $.ajax + url: build_url + dataType: "json" + success: (build) => + if build.status == "running" + $('#build-trace code').html build.trace_html + $('#build-trace code').append '<i class="fa fa-refresh fa-spin"/>' + @checkAutoscroll() + else + Turbolinks.visit build_url + , 4000 + + checkAutoscroll: -> + $("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state") + +@CiBuild = CiBuild diff --git a/app/assets/javascripts/ci/projects.js.coffee b/app/assets/javascripts/ci/projects.js.coffee new file mode 100644 index 00000000000..7e028b4e115 --- /dev/null +++ b/app/assets/javascripts/ci/projects.js.coffee @@ -0,0 +1,6 @@ +$(document).on 'click', '.badge-codes-toggle', -> + $('.badge-codes-block').toggleClass("hide") + return false + +$(document).on 'click', '.sync-now', -> + $(this).find('i').addClass('fa-spin') diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee index a0dcaa8c27a..6f789e668af 100644 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ b/app/assets/javascripts/dropzone_input.js.coffee @@ -167,6 +167,7 @@ class @DropzoneInput dataType: "json" ).success (data) -> preview.html data.body + preview.syntaxHighlight() renderReferencedUsers data.references.users diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee index 176d9cabefa..c4d3e619f5e 100644 --- a/app/assets/javascripts/issuable_context.js.coffee +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -11,12 +11,13 @@ class @IssuableContext $(this).submit() $('.issuable-details').waitForImages -> + $('.issuable-affix').on 'affix.bs.affix', -> + $(@).width($(@).outerWidth()) + .on 'affixed-top.bs.affix affixed-bottom.bs.affix', -> + $(@).width('') + $('.issuable-affix').affix offset: top: -> @top = ($('.issuable-affix').offset().top - 70) bottom: -> @bottom = $('.footer').outerHeight(true) - $('.issuable-affix').on 'affix.bs.affix', -> - $(@).width($(@).outerWidth()) - .on 'affixed-top.bs.affix affixed-bottom.bs.affix', -> - $(@).width('') diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index b7f2c63c5a7..ce638c2641b 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -122,8 +122,9 @@ class @Notes # or skip if rendered if @isNewNote(note) @note_ids.push(note.id) - $('ul.main-notes-list').append(note.html) - $('.js-syntax-highlight').syntaxHighlight() + $('ul.main-notes-list'). + append(note.html). + syntaxHighlight() @initTaskList() ### diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index 39a433dfc91..0ea8fffce07 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -24,3 +24,19 @@ class @Project $.cookie('hide_no_password_message', 'false', { path: path }) $(@).parents('.no-password-message').remove() e.preventDefault() + + $('.update-notification').on 'click', (e) -> + e.preventDefault() + notification_level = $(@).data 'notification-level' + $('#notification_level').val(notification_level) + $('#notification-form').submit() + label = null + switch notification_level + when 0 then label = ' Disabled ' + when 1 then label = ' Participating ' + when 2 then label = ' Watching ' + when 3 then label = ' Global ' + when 4 then label = ' On Mention ' + $('#notifications-button').empty().append("<i class='fa fa-bell'></i>" + label + "<i class='fa fa-angle-down'></i>") + $(@).parents('ul').find('li.active').removeClass 'active' + $(@).parent().addClass 'active'
\ No newline at end of file diff --git a/app/assets/javascripts/syntax_highlight.coffee b/app/assets/javascripts/syntax_highlight.coffee index 71295cd4b08..980f0232d10 100644 --- a/app/assets/javascripts/syntax_highlight.coffee +++ b/app/assets/javascripts/syntax_highlight.coffee @@ -1,3 +1,5 @@ +# Syntax Highlighter +# # Applies a syntax highlighting color scheme CSS class to any element with the # `js-syntax-highlight` class # @@ -6,7 +8,13 @@ # <div class="js-syntax-highlight"></div> # $.fn.syntaxHighlight = -> - $(this).addClass(gon.user_color_scheme) + if $(this).hasClass('js-syntax-highlight') + # Given the element itself, apply highlighting + $(this).addClass(gon.user_color_scheme) + else + # Given a parent element, recurse to any of its applicable children + $children = $(this).find('.js-syntax-highlight') + $children.syntaxHighlight() if $children.length $(document).on 'ready page:load', -> $('.js-syntax-highlight').syntaxHighlight() diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 46f7feddf8d..d9ede637944 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -61,3 +61,9 @@ * Styles for JS behaviors. */ @import "behaviors.scss"; + +/** + * CI specific styles: + */ +@import "ci/**/*"; + diff --git a/app/assets/stylesheets/base/gl_bootstrap.scss b/app/assets/stylesheets/base/gl_bootstrap.scss index ae72c5b8d97..eb8d23d6453 100644 --- a/app/assets/stylesheets/base/gl_bootstrap.scss +++ b/app/assets/stylesheets/base/gl_bootstrap.scss @@ -156,10 +156,16 @@ * Add some extra stuff to panels * */ + +.container-blank .panel .panel-heading { + font-size: 17px; + line-height: 38px; +} + .panel { - .panel-heading { - font-weight: bold; + box-shadow: none; + .panel-heading { .panel-head-actions { position: relative; top: -5px; @@ -182,6 +188,10 @@ .pagination { margin: 0; } + + .btn { + min-width: 124px; + } } &.panel-small { @@ -209,6 +219,12 @@ } } +.alert-help { + background-color: $background-color; + border: 1px solid $border-color; + color: $gl-gray; +} + // Typography ================================================================= .text-primary, diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/base/gl_variables.scss index d18b48eaca9..7378d404008 100644 --- a/app/assets/stylesheets/base/gl_variables.scss +++ b/app/assets/stylesheets/base/gl_variables.scss @@ -65,20 +65,20 @@ $legend-color: $text-color; // //## -$pagination-color: #fff; -$pagination-bg: $brand-success; +$pagination-color: $gl-gray; +$pagination-bg: $background-color; $pagination-border: transparent; $pagination-hover-color: #fff; -$pagination-hover-bg: darken($brand-success, 15%); +$pagination-hover-bg: $brand-info; $pagination-hover-border: transparent; $pagination-active-color: #fff; -$pagination-active-bg: darken($brand-success, 15%); +$pagination-active-bg: $brand-info; $pagination-active-border: transparent; -$pagination-disabled-color: #b4bcc2; -$pagination-disabled-bg: lighten($brand-success, 15%); +$pagination-disabled-color: #fff; +$pagination-disabled-bg: lighten($brand-info, 15%); $pagination-disabled-border: transparent; @@ -114,11 +114,12 @@ $alert-border-radius: 0; // //## -$panel-border-radius: 0; -$panel-default-text: $text-color; -$panel-default-border: #E7E9ED; -$panel-default-heading-bg: #F8FAFC; - +$panel-border-radius: 2px; +$panel-default-text: $text-color; +$panel-default-border: $border-color; +$panel-default-heading-bg: $background-color; +$panel-footer-bg: $background-color; +$panel-inner-border: $border-color; //== Wells // diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/base/mixins.scss index 0f661d6b1b6..c74a6d39824 100644 --- a/app/assets/stylesheets/base/mixins.scss +++ b/app/assets/stylesheets/base/mixins.scss @@ -55,10 +55,10 @@ } @mixin md-typography { - color: #444; + color: $md-text-color; a { - color: #3084bb; + color: $md-link-color; } img { @@ -93,46 +93,88 @@ } h1 { - margin-top: 45px; - font-size: 2.5em; + font-size: 1.3em; + font-weight: 600; + margin: 24px 0 12px 0; + padding: 0 0 10px 0; + border-bottom: 1px solid #e7e9ed; + color: #313236; } h2 { - margin-top: 40px; - font-size: 2em; + font-size: 1.2em; + font-weight: 600; + margin: 24px 0 12px 0; + color: #313236; } h3 { - margin-top: 35px; - font-size: 1.5em; + margin: 24px 0 12px 0; + font-size: 1.25em; } h4 { - margin-top: 30px; - font-size: 1.2em; + margin: 24px 0 12px 0; + font-size: 1.1em; + } + + h5 { + margin: 24px 0 12px 0; + font-size: 1em; + } + + h6 { + margin: 24px 0 12px 0; + font-size: 0.90em; } blockquote { - color: #888; + padding: 8px 21px; + margin: 12px 0 12px; + border-left: 3px solid #e7e9ed; + } + + blockquote p { + color: #7f8fa4 !important; font-size: 15px; line-height: 1.5; } + p { + color:#5c5d5e; + margin:6px 0 0 0; + } + table { @extend .table; @extend .table-bordered; + margin: 12px 0 12px 0; + color: #5c5d5e; th { - background: #EEE; + background: #f8fafc; } } + pre { + margin: 12px 0 12px 0 !important; + background-color: #f8fafc !important; + font-size: 13px !important; + color: #5b6169 !important; + line-height: 1.6em !important; + @include border-radius(2px); + } + p > code { - font-size: inherit; font-weight: inherit; } + + ul { + color: #5c5d5e; + } + li { - line-height: 1.5; + line-height: 1.6em; } a[href*="/uploads/"], a[href*="storage.googleapis.com/google-code-attachments/"] { @@ -152,6 +194,7 @@ } } + @mixin str-truncated($max_width: 82%) { display: inline-block; overflow: hidden; @@ -183,7 +226,7 @@ &.active { background: #f9f9f9; a { - font-weight: bold; + font-weight: 600; } } @@ -251,3 +294,8 @@ } } } + +.fa-align { + top: 20px; + position: relative; +} diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss index 21462b31127..f6bdea9a897 100644 --- a/app/assets/stylesheets/base/variables.scss +++ b/app/assets/stylesheets/base/variables.scss @@ -2,6 +2,8 @@ $hover: #FFFAF1; $gl-text-color: #54565b; $gl-header-color: #4c4e54; $gl-link-color: #333c48; +$md-text-color: #444; +$md-link-color: #3084bb; $nprogress-color: #c0392b; $gl-font-size: 15px; $list-font-size: 15px; @@ -10,8 +12,8 @@ $sidebar_width: 230px; $avatar_radius: 50%; $code_font_size: 13px; $code_line_height: 1.5; -$border-color: #E7E9ED; -$background-color: #F8FAFC; +$border-color: #dce0e6; +$background-color: #F7F8FA; $header-height: 58px; $fixed-layout-width: 1200px; $gl-gray: #7f8fa4; diff --git a/app/assets/stylesheets/ci/builds.scss b/app/assets/stylesheets/ci/builds.scss new file mode 100644 index 00000000000..a11a935b54d --- /dev/null +++ b/app/assets/stylesheets/ci/builds.scss @@ -0,0 +1,70 @@ +.ci-body { + pre.trace { + background: #111111; + color: #fff; + font-family: $monospace_font; + white-space: pre; + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ + overflow: auto; + overflow-y: hidden; + font-size: 12px; + + .fa-refresh { + font-size: 24px; + margin-left: 20px; + } + } + + .autoscroll-container { + position: fixed; + bottom: 10px; + right: 20px; + z-index: 100; + } + + .scroll-controls { + position: fixed; + bottom: 10px; + left: 250px; + z-index: 100; + + a { + display: block; + margin-bottom: 5px; + } + } + + .page-sidebar-collapsed { + .scroll-controls { + left: 70px; + } + } + + .build-widget { + padding: 10px; + background: $background-color; + margin-bottom: 20px; + border-radius: 4px; + + .title { + margin-top: 0; + color: #666; + line-height: 1.5; + } + .attr-name { + color: #777; + } + } + + .alert-disabled { + background: $background-color; + + a { + color: #3084bb !important; + } + } +} diff --git a/app/assets/stylesheets/ci/lint.scss b/app/assets/stylesheets/ci/lint.scss new file mode 100644 index 00000000000..6d2bd33b28b --- /dev/null +++ b/app/assets/stylesheets/ci/lint.scss @@ -0,0 +1,10 @@ +.ci-body { + .incorrect-syntax{ + font-size: 19px; + color: red; + } + .correct-syntax{ + font-size: 19px; + color: #47a447; + } +} diff --git a/app/assets/stylesheets/ci/projects.scss b/app/assets/stylesheets/ci/projects.scss new file mode 100644 index 00000000000..8c5273abcda --- /dev/null +++ b/app/assets/stylesheets/ci/projects.scss @@ -0,0 +1,59 @@ +.ci-body { + .project-title { + margin: 0; + color: #444; + font-size: 20px; + line-height: 1.5; + } + + .wide-table-holder { + margin-left: -$gl-padding; + margin-right: -$gl-padding; + } + + .builds, + .projects-table { + .light { + border-color: $border-color; + } + + th, td { + padding: 10px $gl-padding; + } + + td { + color: $gl-gray; + vertical-align: middle !important; + + a { + font-weight: normal; + text-decoration: none; + } + } + } + + .commit-info { + .attr-name { + margin-right: 5px; + } + + pre.commit-message { + background: none; + padding: 0; + margin: 0; + border: none; + margin: 20px 0; + border-radius: 0; + } + } + + .loading{ + font-size: 20px; + } + + .ci-charts { + fieldset { + margin-bottom: 16px; + } + } +} diff --git a/app/assets/stylesheets/ci/runners.scss b/app/assets/stylesheets/ci/runners.scss new file mode 100644 index 00000000000..2b15ab83129 --- /dev/null +++ b/app/assets/stylesheets/ci/runners.scss @@ -0,0 +1,36 @@ +.ci-body { + .runner-state { + padding: 6px 12px; + margin-right: 10px; + color: #FFF; + + &.runner-state-shared { + background: #32b186; + } + &.runner-state-specific { + background: #3498db; + } + } + + .runner-status-online { + color: green; + } + + .runner-status-offline { + color: gray; + } + + .runner-status-paused { + color: red; + } + + .runner { + .btn { + padding: 1px 6px; + } + + h4 { + font-weight: normal; + } + } +} diff --git a/app/assets/stylesheets/ci/status.scss b/app/assets/stylesheets/ci/status.scss new file mode 100644 index 00000000000..a7d3b2197f1 --- /dev/null +++ b/app/assets/stylesheets/ci/status.scss @@ -0,0 +1,37 @@ +.ci-status { + padding: 2px 7px; + margin-right: 5px; + border: 1px solid #EEE; + white-space: nowrap; + @include border-radius(4px); + + &:hover { + text-decoration: none; + } + + &.ci-failed { + color: $gl-danger; + border-color: $gl-danger; + } + + &.ci-success { + color: $gl-success; + border-color: $gl-success; + } + + &.ci-info { + color: $gl-info; + border-color: $gl-info; + } + + &.ci-disabled { + color: $gl-gray; + border-color: $gl-gray; + } + + &.ci-pending, + &.ci-running { + color: $gl-warning; + border-color: $gl-warning; + } +} diff --git a/app/assets/stylesheets/ci/xterm.scss b/app/assets/stylesheets/ci/xterm.scss new file mode 100644 index 00000000000..532dede0b23 --- /dev/null +++ b/app/assets/stylesheets/ci/xterm.scss @@ -0,0 +1,906 @@ +.ci-body { + // color codes are based on http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg + // see also: https://gist.github.com/jasonm23/2868981 + + $black: #000000; + $red: #cd0000; + $green: #00cd00; + $yellow: #cdcd00; + $blue: #0000ee; // according to wikipedia, this is the xterm standard + //$blue: #1e90ff; // this is used by all the terminals I tried (when configured with the xterm color profile) + $magenta: #cd00cd; + $cyan: #00cdcd; + $white: #e5e5e5; + $l-black: #7f7f7f; + $l-red: #ff0000; + $l-green: #00ff00; + $l-yellow: #ffff00; + $l-blue: #5c5cff; + $l-magenta: #ff00ff; + $l-cyan: #00ffff; + $l-white: #ffffff; + + .term-bold { + font-weight: bold; + } + .term-italic { + font-style: italic; + } + .term-conceal { + visibility: hidden; + } + .term-underline { + text-decoration: underline; + } + .term-cross { + text-decoration: line-through; + } + + .term-fg-black { + color: $black; + } + .term-fg-red { + color: $red; + } + .term-fg-green { + color: $green; + } + .term-fg-yellow { + color: $yellow; + } + .term-fg-blue { + color: $blue; + } + .term-fg-magenta { + color: $magenta; + } + .term-fg-cyan { + color: $cyan; + } + .term-fg-white { + color: $white; + } + .term-fg-l-black { + color: $l-black; + } + .term-fg-l-red { + color: $l-red; + } + .term-fg-l-green { + color: $l-green; + } + .term-fg-l-yellow { + color: $l-yellow; + } + .term-fg-l-blue { + color: $l-blue; + } + .term-fg-l-magenta { + color: $l-magenta; + } + .term-fg-l-cyan { + color: $l-cyan; + } + .term-fg-l-white { + color: $l-white; + } + + .term-bg-black { + background-color: $black; + } + .term-bg-red { + background-color: $red; + } + .term-bg-green { + background-color: $green; + } + .term-bg-yellow { + background-color: $yellow; + } + .term-bg-blue { + background-color: $blue; + } + .term-bg-magenta { + background-color: $magenta; + } + .term-bg-cyan { + background-color: $cyan; + } + .term-bg-white { + background-color: $white; + } + .term-bg-l-black { + background-color: $l-black; + } + .term-bg-l-red { + background-color: $l-red; + } + .term-bg-l-green { + background-color: $l-green; + } + .term-bg-l-yellow { + background-color: $l-yellow; + } + .term-bg-l-blue { + background-color: $l-blue; + } + .term-bg-l-magenta { + background-color: $l-magenta; + } + .term-bg-l-cyan { + background-color: $l-cyan; + } + .term-bg-l-white { + background-color: $l-white; + } + + + .xterm-fg-0 { + color: #000000; + } + .xterm-fg-1 { + color: #800000; + } + .xterm-fg-2 { + color: #008000; + } + .xterm-fg-3 { + color: #808000; + } + .xterm-fg-4 { + color: #000080; + } + .xterm-fg-5 { + color: #800080; + } + .xterm-fg-6 { + color: #008080; + } + .xterm-fg-7 { + color: #c0c0c0; + } + .xterm-fg-8 { + color: #808080; + } + .xterm-fg-9 { + color: #ff0000; + } + .xterm-fg-10 { + color: #00ff00; + } + .xterm-fg-11 { + color: #ffff00; + } + .xterm-fg-12 { + color: #0000ff; + } + .xterm-fg-13 { + color: #ff00ff; + } + .xterm-fg-14 { + color: #00ffff; + } + .xterm-fg-15 { + color: #ffffff; + } + .xterm-fg-16 { + color: #000000; + } + .xterm-fg-17 { + color: #00005f; + } + .xterm-fg-18 { + color: #000087; + } + .xterm-fg-19 { + color: #0000af; + } + .xterm-fg-20 { + color: #0000d7; + } + .xterm-fg-21 { + color: #0000ff; + } + .xterm-fg-22 { + color: #005f00; + } + .xterm-fg-23 { + color: #005f5f; + } + .xterm-fg-24 { + color: #005f87; + } + .xterm-fg-25 { + color: #005faf; + } + .xterm-fg-26 { + color: #005fd7; + } + .xterm-fg-27 { + color: #005fff; + } + .xterm-fg-28 { + color: #008700; + } + .xterm-fg-29 { + color: #00875f; + } + .xterm-fg-30 { + color: #008787; + } + .xterm-fg-31 { + color: #0087af; + } + .xterm-fg-32 { + color: #0087d7; + } + .xterm-fg-33 { + color: #0087ff; + } + .xterm-fg-34 { + color: #00af00; + } + .xterm-fg-35 { + color: #00af5f; + } + .xterm-fg-36 { + color: #00af87; + } + .xterm-fg-37 { + color: #00afaf; + } + .xterm-fg-38 { + color: #00afd7; + } + .xterm-fg-39 { + color: #00afff; + } + .xterm-fg-40 { + color: #00d700; + } + .xterm-fg-41 { + color: #00d75f; + } + .xterm-fg-42 { + color: #00d787; + } + .xterm-fg-43 { + color: #00d7af; + } + .xterm-fg-44 { + color: #00d7d7; + } + .xterm-fg-45 { + color: #00d7ff; + } + .xterm-fg-46 { + color: #00ff00; + } + .xterm-fg-47 { + color: #00ff5f; + } + .xterm-fg-48 { + color: #00ff87; + } + .xterm-fg-49 { + color: #00ffaf; + } + .xterm-fg-50 { + color: #00ffd7; + } + .xterm-fg-51 { + color: #00ffff; + } + .xterm-fg-52 { + color: #5f0000; + } + .xterm-fg-53 { + color: #5f005f; + } + .xterm-fg-54 { + color: #5f0087; + } + .xterm-fg-55 { + color: #5f00af; + } + .xterm-fg-56 { + color: #5f00d7; + } + .xterm-fg-57 { + color: #5f00ff; + } + .xterm-fg-58 { + color: #5f5f00; + } + .xterm-fg-59 { + color: #5f5f5f; + } + .xterm-fg-60 { + color: #5f5f87; + } + .xterm-fg-61 { + color: #5f5faf; + } + .xterm-fg-62 { + color: #5f5fd7; + } + .xterm-fg-63 { + color: #5f5fff; + } + .xterm-fg-64 { + color: #5f8700; + } + .xterm-fg-65 { + color: #5f875f; + } + .xterm-fg-66 { + color: #5f8787; + } + .xterm-fg-67 { + color: #5f87af; + } + .xterm-fg-68 { + color: #5f87d7; + } + .xterm-fg-69 { + color: #5f87ff; + } + .xterm-fg-70 { + color: #5faf00; + } + .xterm-fg-71 { + color: #5faf5f; + } + .xterm-fg-72 { + color: #5faf87; + } + .xterm-fg-73 { + color: #5fafaf; + } + .xterm-fg-74 { + color: #5fafd7; + } + .xterm-fg-75 { + color: #5fafff; + } + .xterm-fg-76 { + color: #5fd700; + } + .xterm-fg-77 { + color: #5fd75f; + } + .xterm-fg-78 { + color: #5fd787; + } + .xterm-fg-79 { + color: #5fd7af; + } + .xterm-fg-80 { + color: #5fd7d7; + } + .xterm-fg-81 { + color: #5fd7ff; + } + .xterm-fg-82 { + color: #5fff00; + } + .xterm-fg-83 { + color: #5fff5f; + } + .xterm-fg-84 { + color: #5fff87; + } + .xterm-fg-85 { + color: #5fffaf; + } + .xterm-fg-86 { + color: #5fffd7; + } + .xterm-fg-87 { + color: #5fffff; + } + .xterm-fg-88 { + color: #870000; + } + .xterm-fg-89 { + color: #87005f; + } + .xterm-fg-90 { + color: #870087; + } + .xterm-fg-91 { + color: #8700af; + } + .xterm-fg-92 { + color: #8700d7; + } + .xterm-fg-93 { + color: #8700ff; + } + .xterm-fg-94 { + color: #875f00; + } + .xterm-fg-95 { + color: #875f5f; + } + .xterm-fg-96 { + color: #875f87; + } + .xterm-fg-97 { + color: #875faf; + } + .xterm-fg-98 { + color: #875fd7; + } + .xterm-fg-99 { + color: #875fff; + } + .xterm-fg-100 { + color: #878700; + } + .xterm-fg-101 { + color: #87875f; + } + .xterm-fg-102 { + color: #878787; + } + .xterm-fg-103 { + color: #8787af; + } + .xterm-fg-104 { + color: #8787d7; + } + .xterm-fg-105 { + color: #8787ff; + } + .xterm-fg-106 { + color: #87af00; + } + .xterm-fg-107 { + color: #87af5f; + } + .xterm-fg-108 { + color: #87af87; + } + .xterm-fg-109 { + color: #87afaf; + } + .xterm-fg-110 { + color: #87afd7; + } + .xterm-fg-111 { + color: #87afff; + } + .xterm-fg-112 { + color: #87d700; + } + .xterm-fg-113 { + color: #87d75f; + } + .xterm-fg-114 { + color: #87d787; + } + .xterm-fg-115 { + color: #87d7af; + } + .xterm-fg-116 { + color: #87d7d7; + } + .xterm-fg-117 { + color: #87d7ff; + } + .xterm-fg-118 { + color: #87ff00; + } + .xterm-fg-119 { + color: #87ff5f; + } + .xterm-fg-120 { + color: #87ff87; + } + .xterm-fg-121 { + color: #87ffaf; + } + .xterm-fg-122 { + color: #87ffd7; + } + .xterm-fg-123 { + color: #87ffff; + } + .xterm-fg-124 { + color: #af0000; + } + .xterm-fg-125 { + color: #af005f; + } + .xterm-fg-126 { + color: #af0087; + } + .xterm-fg-127 { + color: #af00af; + } + .xterm-fg-128 { + color: #af00d7; + } + .xterm-fg-129 { + color: #af00ff; + } + .xterm-fg-130 { + color: #af5f00; + } + .xterm-fg-131 { + color: #af5f5f; + } + .xterm-fg-132 { + color: #af5f87; + } + .xterm-fg-133 { + color: #af5faf; + } + .xterm-fg-134 { + color: #af5fd7; + } + .xterm-fg-135 { + color: #af5fff; + } + .xterm-fg-136 { + color: #af8700; + } + .xterm-fg-137 { + color: #af875f; + } + .xterm-fg-138 { + color: #af8787; + } + .xterm-fg-139 { + color: #af87af; + } + .xterm-fg-140 { + color: #af87d7; + } + .xterm-fg-141 { + color: #af87ff; + } + .xterm-fg-142 { + color: #afaf00; + } + .xterm-fg-143 { + color: #afaf5f; + } + .xterm-fg-144 { + color: #afaf87; + } + .xterm-fg-145 { + color: #afafaf; + } + .xterm-fg-146 { + color: #afafd7; + } + .xterm-fg-147 { + color: #afafff; + } + .xterm-fg-148 { + color: #afd700; + } + .xterm-fg-149 { + color: #afd75f; + } + .xterm-fg-150 { + color: #afd787; + } + .xterm-fg-151 { + color: #afd7af; + } + .xterm-fg-152 { + color: #afd7d7; + } + .xterm-fg-153 { + color: #afd7ff; + } + .xterm-fg-154 { + color: #afff00; + } + .xterm-fg-155 { + color: #afff5f; + } + .xterm-fg-156 { + color: #afff87; + } + .xterm-fg-157 { + color: #afffaf; + } + .xterm-fg-158 { + color: #afffd7; + } + .xterm-fg-159 { + color: #afffff; + } + .xterm-fg-160 { + color: #d70000; + } + .xterm-fg-161 { + color: #d7005f; + } + .xterm-fg-162 { + color: #d70087; + } + .xterm-fg-163 { + color: #d700af; + } + .xterm-fg-164 { + color: #d700d7; + } + .xterm-fg-165 { + color: #d700ff; + } + .xterm-fg-166 { + color: #d75f00; + } + .xterm-fg-167 { + color: #d75f5f; + } + .xterm-fg-168 { + color: #d75f87; + } + .xterm-fg-169 { + color: #d75faf; + } + .xterm-fg-170 { + color: #d75fd7; + } + .xterm-fg-171 { + color: #d75fff; + } + .xterm-fg-172 { + color: #d78700; + } + .xterm-fg-173 { + color: #d7875f; + } + .xterm-fg-174 { + color: #d78787; + } + .xterm-fg-175 { + color: #d787af; + } + .xterm-fg-176 { + color: #d787d7; + } + .xterm-fg-177 { + color: #d787ff; + } + .xterm-fg-178 { + color: #d7af00; + } + .xterm-fg-179 { + color: #d7af5f; + } + .xterm-fg-180 { + color: #d7af87; + } + .xterm-fg-181 { + color: #d7afaf; + } + .xterm-fg-182 { + color: #d7afd7; + } + .xterm-fg-183 { + color: #d7afff; + } + .xterm-fg-184 { + color: #d7d700; + } + .xterm-fg-185 { + color: #d7d75f; + } + .xterm-fg-186 { + color: #d7d787; + } + .xterm-fg-187 { + color: #d7d7af; + } + .xterm-fg-188 { + color: #d7d7d7; + } + .xterm-fg-189 { + color: #d7d7ff; + } + .xterm-fg-190 { + color: #d7ff00; + } + .xterm-fg-191 { + color: #d7ff5f; + } + .xterm-fg-192 { + color: #d7ff87; + } + .xterm-fg-193 { + color: #d7ffaf; + } + .xterm-fg-194 { + color: #d7ffd7; + } + .xterm-fg-195 { + color: #d7ffff; + } + .xterm-fg-196 { + color: #ff0000; + } + .xterm-fg-197 { + color: #ff005f; + } + .xterm-fg-198 { + color: #ff0087; + } + .xterm-fg-199 { + color: #ff00af; + } + .xterm-fg-200 { + color: #ff00d7; + } + .xterm-fg-201 { + color: #ff00ff; + } + .xterm-fg-202 { + color: #ff5f00; + } + .xterm-fg-203 { + color: #ff5f5f; + } + .xterm-fg-204 { + color: #ff5f87; + } + .xterm-fg-205 { + color: #ff5faf; + } + .xterm-fg-206 { + color: #ff5fd7; + } + .xterm-fg-207 { + color: #ff5fff; + } + .xterm-fg-208 { + color: #ff8700; + } + .xterm-fg-209 { + color: #ff875f; + } + .xterm-fg-210 { + color: #ff8787; + } + .xterm-fg-211 { + color: #ff87af; + } + .xterm-fg-212 { + color: #ff87d7; + } + .xterm-fg-213 { + color: #ff87ff; + } + .xterm-fg-214 { + color: #ffaf00; + } + .xterm-fg-215 { + color: #ffaf5f; + } + .xterm-fg-216 { + color: #ffaf87; + } + .xterm-fg-217 { + color: #ffafaf; + } + .xterm-fg-218 { + color: #ffafd7; + } + .xterm-fg-219 { + color: #ffafff; + } + .xterm-fg-220 { + color: #ffd700; + } + .xterm-fg-221 { + color: #ffd75f; + } + .xterm-fg-222 { + color: #ffd787; + } + .xterm-fg-223 { + color: #ffd7af; + } + .xterm-fg-224 { + color: #ffd7d7; + } + .xterm-fg-225 { + color: #ffd7ff; + } + .xterm-fg-226 { + color: #ffff00; + } + .xterm-fg-227 { + color: #ffff5f; + } + .xterm-fg-228 { + color: #ffff87; + } + .xterm-fg-229 { + color: #ffffaf; + } + .xterm-fg-230 { + color: #ffffd7; + } + .xterm-fg-231 { + color: #ffffff; + } + .xterm-fg-232 { + color: #080808; + } + .xterm-fg-233 { + color: #121212; + } + .xterm-fg-234 { + color: #1c1c1c; + } + .xterm-fg-235 { + color: #262626; + } + .xterm-fg-236 { + color: #303030; + } + .xterm-fg-237 { + color: #3a3a3a; + } + .xterm-fg-238 { + color: #444444; + } + .xterm-fg-239 { + color: #4e4e4e; + } + .xterm-fg-240 { + color: #585858; + } + .xterm-fg-241 { + color: #626262; + } + .xterm-fg-242 { + color: #6c6c6c; + } + .xterm-fg-243 { + color: #767676; + } + .xterm-fg-244 { + color: #808080; + } + .xterm-fg-245 { + color: #8a8a8a; + } + .xterm-fg-246 { + color: #949494; + } + .xterm-fg-247 { + color: #9e9e9e; + } + .xterm-fg-248 { + color: #a8a8a8; + } + .xterm-fg-249 { + color: #b2b2b2; + } + .xterm-fg-250 { + color: #bcbcbc; + } + .xterm-fg-251 { + color: #c6c6c6; + } + .xterm-fg-252 { + color: #d0d0d0; + } + .xterm-fg-253 { + color: #dadada; + } + .xterm-fg-254 { + color: #e4e4e4; + } + .xterm-fg-255 { + color: #eeeeee; + } +} diff --git a/app/assets/stylesheets/generic/avatar.scss b/app/assets/stylesheets/generic/avatar.scss index 221cb6a04a5..36e582d4854 100644 --- a/app/assets/stylesheets/generic/avatar.scss +++ b/app/assets/stylesheets/generic/avatar.scss @@ -28,6 +28,7 @@ &.s48 { width: 48px; height: 48px; margin-right: 10px; } &.s60 { width: 60px; height: 60px; margin-right: 12px; } &.s90 { width: 90px; height: 90px; margin-right: 15px; } + &.s110 { width: 110px; height: 110px; margin-right: 15px; } &.s140 { width: 140px; height: 140px; margin-right: 20px; } &.s160 { width: 160px; height: 160px; margin-right: 20px; } } @@ -42,6 +43,7 @@ &.s32 { font-size: 22px; line-height: 32px; } &.s60 { font-size: 32px; line-height: 60px; } &.s90 { font-size: 36px; line-height: 90px; } + &.s110 { font-size: 40px; line-height: 112px; font-weight: 300; } &.s140 { font-size: 72px; line-height: 140px; } &.s160 { font-size: 96px; line-height: 160px; } } diff --git a/app/assets/stylesheets/generic/blocks.scss b/app/assets/stylesheets/generic/blocks.scss index 27a4c4db8c8..6ce34b5c3e8 100644 --- a/app/assets/stylesheets/generic/blocks.scss +++ b/app/assets/stylesheets/generic/blocks.scss @@ -20,18 +20,27 @@ .gray-content-block { margin: -$gl-padding; - background-color: #f8fafc; + background-color: $background-color; padding: $gl-padding; margin-bottom: 0px; - border-top: 1px solid #e7e9ed; - border-bottom: 1px solid #e7e9ed; + border-top: 1px solid $border-color; + border-bottom: 1px solid $border-color; color: $gl-gray; + &.top-block { + border-top: none; + } + &.middle-block { margin-top: 0; margin-bottom: 0; } + &.clear-block { + margin-bottom: $gl-padding - 1px; + padding-bottom: $gl-padding; + } + &.second-block { margin-top: -1px; margin-bottom: 0; @@ -39,6 +48,7 @@ &.footer-block { margin-top: 0; + border-bottom: none; margin-bottom: -$gl-padding; } @@ -47,6 +57,6 @@ } .oneline { - line-height: 44px; + line-height: 42px; } } diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss index e8237509092..cf76f538e01 100644 --- a/app/assets/stylesheets/generic/buttons.scss +++ b/app/assets/stylesheets/generic/buttons.scss @@ -1,3 +1,6 @@ +body { + text-rendering: geometricPrecision; +} .btn { @extend .btn-default; @@ -10,7 +13,7 @@ } &.btn-save { - @extend .btn-primary; + @extend .btn-success; } &.btn-remove { @@ -88,3 +91,138 @@ } } } + +@mixin btn-info { + @include border-radius(2px); + + border-width: 1px; + border-style: solid; + text-transform: uppercase; + font-size: 13px; + font-weight: 600; + line-height: 18px; + padding: 11px 16px; + letter-spacing: .4px; + + &:hover { + border-width: 1px; + border-style: solid; + } + + &:focus { + border-width: 1px; + border-style: solid; + } + + &:active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + border-width: 1px; + border-style: solid; + } +} + +@mixin btn-middle { + @include border-radius(2px); + + border-width: 1px; + border-style: solid; + text-transform: uppercase; + font-size: 13px; + font-weight: 600; + line-height: 18px; + padding: 11px 24px; + letter-spacing: .4px; + + &:hover { + border-width: 1px; + border-style: solid; + } + + &:focus { + border-width: 1px; + border-style: solid; + } + + &:active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + border-width: 1px; + border-style: solid; + } +} + + +@mixin btn-green { + background-color: #28b061; + border: 1px solid #26a65c; + color: #fff; + + &:hover { + background-color: #26ab5d; + border: 1px solid #229954; + color: #fff; + } + + &:focus { + background-color: #26ab5d; + border: 1px solid #229954; + color: #fff; + } + + &:active { + @include box-shadow (inset 0 0 4px rgba(0, 0, 0, 0.12)); + + background-color: #23a158 !important; + border: 1px solid #229954 !important; + color: #fff !important; + } +} + +/*Butons*/ + +@mixin bnt-project { + background-color: #f0f2f5; + border-color: #dce0e5; + color: #313236; + + &:hover { + border-color:#dce0e5; + background-color: #ebeef2; + color: #313236; + } + + &:focus { + border-color: #dce0e5; + background-color: #ebeef2; + color: #313236; + } + + &:active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + + color: #313236 !important; + border-color: #c6cacf !important; + background-color: #e4e7ed !important; + } +} + +@mixin btn-remove { + background-color: #f72e60; + border-color: #ee295a; + + &:hover { + background-color: #e82757; + border-color: #e32555; + } + + &:focus { + background-color: #e82757; + border-color: #e32555; + } + + &:active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + background-color: #d42450 !important; + border-color: #e12554 !important; + } + +}
\ No newline at end of file diff --git a/app/assets/stylesheets/generic/callout.scss b/app/assets/stylesheets/generic/callout.scss new file mode 100644 index 00000000000..f1699d21c9b --- /dev/null +++ b/app/assets/stylesheets/generic/callout.scss @@ -0,0 +1,45 @@ +/* + * Callouts from Bootstrap3 docs + * + * Not quite alerts, but custom and helpful notes for folks reading the docs. + * Requires a base and modifier class. + */ + +/* Common styles for all types */ +.bs-callout { + margin: 20px 0; + padding: 20px; + border-left: 3px solid #eee; + color: #666; + background: #f9f9f9; +} +.bs-callout h4 { + margin-top: 0; + margin-bottom: 5px; +} +.bs-callout p:last-child { + margin-bottom: 0; +} + +/* Variations */ +.bs-callout-danger { + background-color: #fdf7f7; + border-color: #eed3d7; + color: #b94a48; +} +.bs-callout-warning { + background-color: #faf8f0; + border-color: #faebcc; + color: #8a6d3b; +} +.bs-callout-info { + background-color: #f4f8fa; + border-color: #bce8f1; + color: #34789a; +} +.bs-callout-success { + background-color: #dff0d8; + border-color: #5cA64d; + color: #3c763d; +} + diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index 5e191d5dd4a..96fb791c242 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -302,7 +302,7 @@ table { } .btn-sign-in { - margin-top: 15px; + margin-top: 8px; text-shadow: none; } @@ -313,7 +313,7 @@ table { } .wiki .highlight, .note-body .highlight { - margin-bottom: 9px; + margin: 12px 0 12px 0; } .wiki .code { @@ -377,4 +377,16 @@ table { height: 56px; margin-top: -$gl-padding; padding-top: $gl-padding; + + &.no-bottom { + margin-bottom: 0; + } +} + +.dropzone .dz-preview .dz-progress { + border-color: $border-color !important; +} + +.dropzone .dz-preview .dz-progress .dz-upload { + background: $gl-success !important; } diff --git a/app/assets/stylesheets/generic/files.scss b/app/assets/stylesheets/generic/files.scss index f845342c67b..9dd77747884 100644 --- a/app/assets/stylesheets/generic/files.scss +++ b/app/assets/stylesheets/generic/files.scss @@ -3,7 +3,11 @@ * */ .file-holder { - border: 1px solid $border-color; + margin-left: -$gl-padding; + margin-right: -$gl-padding; + border: none; + border-top: 1px solid #E7E9EE; + border-bottom: 1px solid #E7E9EE; margin-bottom: 1em; table { @@ -49,7 +53,7 @@ } &.wiki { - padding: 25px; + padding: $gl-padding; .highlight { margin-bottom: 9px; @@ -90,7 +94,7 @@ border-right: none; } background: #fff; - padding: 8px; + padding: 10px $gl-padding; } .lines { pre { @@ -100,6 +104,33 @@ border: none; } } + img.avatar { + border: 0 none; + float: none; + margin: 0; + padding: 0; + } + td.blame-commit { + background: #f9f9f9; + min-width: 350px; + + .commit-author-link { + color: #888; + } + } + td.blame-numbers { + pre { + color: #AAA; + white-space: pre; + } + background: #f1f1f1; + border-left: 1px solid #DDD; + } + td.lines { + code { + font-family: $monospace_font; + } + } } &.logs { diff --git a/app/assets/stylesheets/generic/header.scss b/app/assets/stylesheets/generic/header.scss index b758a526fbb..543ce41ab52 100644 --- a/app/assets/stylesheets/generic/header.scss +++ b/app/assets/stylesheets/generic/header.scss @@ -26,7 +26,6 @@ header { min-height: $header-height; background-color: #fff; border: none; - border-bottom: 1px solid #EEE; .container-fluid { width: 100% !important; diff --git a/app/assets/stylesheets/generic/pagination.scss b/app/assets/stylesheets/generic/pagination.scss new file mode 100644 index 00000000000..6677f94dafd --- /dev/null +++ b/app/assets/stylesheets/generic/pagination.scss @@ -0,0 +1,34 @@ +.gl-pagination { + border-top: 1px solid $border-color; + background-color: $background-color; + margin: -$gl-padding; + margin-top: 0; + + .pagination { + padding: 0; + margin: 0; + display: block; + + li.first, + li.last, + li.next, + li.prev { + > a { + color: $link-color; + + &:hover { + color: #fff; + } + } + } + + li > a, + li > span { + border: none; + margin: 0; + @include border-radius(0 !important); + padding: 13px 19px; + border-right: 1px solid $border-color; + } + } +} diff --git a/app/assets/stylesheets/generic/sidebar.scss b/app/assets/stylesheets/generic/sidebar.scss index 22720c2e1d5..c5ea3aca7ca 100644 --- a/app/assets/stylesheets/generic/sidebar.scss +++ b/app/assets/stylesheets/generic/sidebar.scss @@ -21,19 +21,24 @@ min-height: 100vh; width: 100%; padding: 20px; - background: #f1f4f8; + background: #EAEBEC; .container-fluid { background: #FFF; padding: $gl-padding; - border: 1px solid #e7e9ed; min-height: 90vh; + + &.container-blank { + background: none; + padding: 0; + border: none; + } } } .nav-sidebar { margin-top: 14 + $header-height; - margin-bottom: 50px; + margin-bottom: 100px; transition-duration: .3s; list-style: none; overflow: hidden; @@ -146,7 +151,6 @@ } .collapse-nav a { - left: 0px; width: $sidebar_collapsed_width; } @@ -165,6 +169,7 @@ width: $sidebar_width; position: fixed; bottom: 0; + left: 0; font-size: 13px; background: transparent; height: 40px; diff --git a/app/assets/stylesheets/generic/timeline.scss b/app/assets/stylesheets/generic/timeline.scss index 668a6f848cc..74bbaabad39 100644 --- a/app/assets/stylesheets/generic/timeline.scss +++ b/app/assets/stylesheets/generic/timeline.scss @@ -4,14 +4,19 @@ margin: 0; padding: 0; - > li { + .timeline-entry { padding: $gl-padding; border-color: #f1f2f4; margin-left: -$gl-padding; margin-right: -$gl-padding; color: $gl-gray; + border-bottom: 1px solid #f1f2f4; border-right: 1px solid #f1f2f4; + &:last-child { + border-bottom: none; + } + .avatar { margin-right: 15px; } @@ -33,6 +38,13 @@ color: $gl-gray !important; } } + + .diff-file { + border: 1px solid $border-color; + border-bottom: none; + margin-left: 0; + margin-right: 0; + } } @media (max-width: $screen-xs-max) { @@ -51,3 +63,8 @@ } } } + +.discussion .timeline-entry { + margin: 0; + border-right: none; +} diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss index 73034c84f9a..6a3cb49baae 100644 --- a/app/assets/stylesheets/generic/typography.scss +++ b/app/assets/stylesheets/generic/typography.scss @@ -2,11 +2,24 @@ * Headers * */ +body { + text-rendering:optimizeLegibility; + -webkit-text-shadow: rgba(255,255,255,0.01) 0 0 1px; +} + .page-title { margin-top: 0px; - line-height: 1.5; - font-weight: normal; - margin-bottom: 5px; + line-height: 1.3; + font-size: 1.25em; + font-weight: 600; +} + +.page-title-empty { + margin-top: 0px; + line-height: 1.3; + font-size: 1.25em; + font-weight: 600; + margin: 12px 7px 12px 7px; } h1, h2, h3, h4, h5, h6 { @@ -55,6 +68,7 @@ a > code { @include md-typography; word-wrap: break-word; + padding: 7px; /* Link to current header. */ h1, h2, h3, h4, h5, h6 { @@ -83,12 +97,19 @@ a > code { } } - ul { + ul,ol { padding: 0; - margin: 0 0 9px 25px !important; + margin: 6px 0 6px 18px !important; + } + ol { + color: #5c5d5e; } } +.md-area { + @include md-typography; +} + .md { @include md-typography; } @@ -101,6 +122,9 @@ textarea.js-gfm-input { font-family: $monospace_font; } +.md-preview { +} + .strikethrough { text-decoration: line-through; -} +}
\ No newline at end of file diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index e0edfb80b42..20a144ef952 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -1,19 +1,24 @@ /* https://github.com/aahan/pygments-github-style */ pre.code.highlight.white, .code.white { + background-color: #f8fafc; + font-size: 13px; + color: #5b6169; + line-height: 1.6em; - background-color: #fff; - color: #333; - - pre.highlight, .line-numbers, .line-numbers a { + background-color: $background-color !important; + color: $gl-gray !important; + } + + pre.highlight { background-color: #fff !important; color: #333 !important; } pre.code { - border-left: 1px solid #bbb; + border-left: 1px solid $border-color; } // highlight line via anchor diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 487b600e31d..5e7e59a6af8 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -1,6 +1,6 @@ .diff-file { - margin-left: -16px; - margin-right: -16px; + margin-left: -$gl-padding; + margin-right: -$gl-padding; border: none; border-bottom: 1px solid #E7E9EE; @@ -8,7 +8,7 @@ position: relative; background: $background-color; border-bottom: 1px solid $border-color; - padding: 10px 15px; + padding: 10px 16px; color: #555; z-index: 10; diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 759ba6b1c22..1d565477dd4 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -9,6 +9,10 @@ width: 100%; } + .ace_gutter-cell { + background-color: $background-color; + } + .cancel-btn { color: #B94A48; &:hover { @@ -32,14 +36,12 @@ .file-title { @extend .monospace; - font-size: 14px; - padding: 5px; } .editor-ref { background: $background-color; padding: 11px 15px; - border-right: 1px solid #CCC; + border-right: 1px solid $border-color; display: inline-block; margin: -5px -5px; margin-right: 10px; @@ -50,5 +52,15 @@ display: inline-block; width: 200px; } + + .form-control { + margin-top: -3px; + } + } + + .form-actions { + margin: -$gl-padding; + margin-top: 0; + padding: $gl-padding } } diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 2b1b747139a..07a38a19fad 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -10,3 +10,9 @@ .milestone-row { @include str-truncated(90%); } + +.dashboard .side .panel .panel-heading .input-group { + .form-control { + height: 42px; + } +}
\ No newline at end of file diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 85804f5ee61..fdc2c3332df 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -109,7 +109,7 @@ .note-edit-form { display: none; - font-size: 13px; + font-size: 15px; .form-actions { padding-left: 20px; @@ -146,9 +146,9 @@ } .discussion-reply-holder { - background: #f9f9f9; + background: $background-color; padding: 10px 15px; - border-top: 1px solid #DDD; + border-top: 1px solid $border-color; } } @@ -170,6 +170,6 @@ background: #FFF; padding: 5px; margin-top: -11px; - border: 1px solid #DDD; + border: 1px solid $border-color; font-size: 13px; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 2544356a5f6..2a77f065aed 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -235,8 +235,6 @@ ul.notes { filter: alpha(opacity=0); &:hover { - width: 38px; - font-size: 20px; background: $gl-info; color: #FFF; @include show-add-diff-note; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 361fd63bc79..31051785676 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -1,3 +1,14 @@ +.alert_holder { + margin: -16px; + + .alert-link { + font-weight: normal; + } +} +.no-ssh-key-message { + background-color: #f28d35; + margin-bottom: 16px; +} .new_project, .edit_project { fieldset.features { @@ -19,45 +30,47 @@ background: #f7f8fa; margin: -$gl-padding; padding: $gl-padding; - padding-top: 40px; - + padding: 44px 0 17px 0; + .project-identicon-holder { - margin-bottom: 15px; - + margin-bottom: 16px; + .avatar, .identicon { margin: 0 auto; float: none; } - + .identicon { @include border-radius(50%); } } - + .project-home-dropdown { margin: 11px 3px 0; } .project-home-desc { h1 { + color: #313236; margin: 0; - margin-bottom: 10px; + margin-bottom: 6px; font-size: 23px; font-weight: normal; } p { - color: #7f8fa4; - display: inline; + color: #5c5d5e; } } .git-clone-holder { - max-width: 600px; - margin: 20px auto; + max-width: 498px; .form-control { background: #FFF; + font-size: 14px; + height: 42px; + margin-left: -1px; } } @@ -67,30 +80,37 @@ color: inherit; } } - + .input-group { + display: inline-table; + position: relative; + top: 17px; + margin-bottom: 44px; + } + .project-repo-buttons { - margin-top: $gl-padding; - margin-bottom: 25px; - + margin-top: 12px; + margin-bottom: 0px; + .btn { - @extend .btn-info; - - text-transform: uppercase; - font-size: 15px; - line-height: 20px; - padding: 8px 14px; - border-radius: 3px; - margin-left: 10px; - + @include bnt-project; + @include btn-info; + .count { - padding-left: 7px; display: inline-block; - margin-left: 7px; } } } } +.split-one { + display: inline-table; + margin-right: 12px; + + a { + margin: -1px !important; + } +} + .git-clone-holder { .project-home-dropdown + & { margin-right: 45px; @@ -100,23 +120,132 @@ cursor: auto; @extend .monospace; background: #FAFAFA; - width: 100%; + width: 101%; } .input-group-addon { - background: #FAFAFA; + background: #f7f8fa; &.git-protocols { padding: 0; border: none; - + .input-group-btn:last-child > .btn { @include border-radius-right(0); + + border-left: 1px solid #c6cacf; + margin-left: -2px !important; } } } } +.projects-search-form { + + .input-group .form-control { + height: 42px; + } +} + +.input-group-btn { + .btn { + @include bnt-project; + @include btn-middle; + + &:hover { + outline: none; + } + + &:focus { + outline: none; + } + + &:active { + outline: none; + } + } + + .active { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + + border: 1px solid #c6cacf !important; + background-color: #e4e7ed !important; + } + + .btn-green { + @include btn-green + } + +} + +.split-repo-buttons { + display: inline-table; + margin: 0 12px 0 12px; + + .btn{ + @include bnt-project; + @include btn-info; + } + + .dropdown-toggle { + margin: -5px; + } +} + +#notification-form { + margin-left: 5px; +} + +.dropdown-new { + margin-left: -5px; +} + +.open > .dropdown-new.btn { + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + + border: 1px solid #c6cacf !important; + background-color: #e4e7ed !important; + text-transform: uppercase; + color: #313236 !important; + font-size: 13px; + font-weight: 600; +} + +.dropdown-menu { + @include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px); + @include border-radius (0px); + + border: none; + padding: 16px 0; + font-size: 14px; + font-weight: 100; + + li a { + color: #5f697a; + line-height: 30px; + + &:hover { + background-color: #3084bb !important; + } + } + + .fa-fw { + margin-right: 8px; + } +} + +.fa-bell { + margin-right: 6px; +} + +.fa-angle-down { + margin-left: 6px; +} + +.project-home-panel .project-home-dropdown { + margin: 13px 0px 0; +} + .project-visibility-level-holder { .radio { margin-bottom: 10px; @@ -204,26 +333,18 @@ ul.nav.nav-projects-tabs { } .fork-namespaces { - .thumbnail { - - &.fork-exists-thumbnail { - border-color: #EEE; - - .caption { - color: #999; - } - } + .fork-thumbnail { + text-align: center; + margin-bottom: $gl-padding; - &.fork-thumbnail { - border-color: #AAA; - - &:hover { - background-color: $hover; - } + .caption { + padding: $gl-padding 0; + min-height: 30px; } - a { - text-decoration: none; + img { + @include border-radius(50%); + max-width: 100px; } } } @@ -241,15 +362,28 @@ table.table.protected-branches-list tr.no-border { .project-stats { text-align: center; - margin-top: 0; + margin-top: 15px; margin-bottom: 0; - padding-top: 5px; - padding-bottom: 0; + padding-top: 10px; + padding-bottom: 4px; ul.nav-pills { display:inline-block; } + + .nav-pills li { + display:inline; + } + .nav > li > a { + @include btn-info; + @include bnt-project; + + background-color: transparent; + border: 1px solid #f7f8fa; + margin-left: 12px; + } + li { display:inline; } @@ -260,11 +394,11 @@ table.table.protected-branches-list tr.no-border { } li.missing a { - color: #bbb; - border: 1px dashed #ccc; + color: #5a6069; + border: 1px dashed #dce0e5; &:hover { - background-color: #FAFAFA; + background-color: #f0f2f5; } } } @@ -282,9 +416,37 @@ pre.light-well { border-bottom: 1px solid #e7e9ed; } +.git-empty { + margin: 0 7px 0 7px; + + h5 { + color: #5c5d5e; + } + + .light-well { + @include border-radius (2px); + + color: #5b6169; + font-size: 13px; + line-height: 1.6em; + } +} + +.prepend-top-20 { + margin-top: 20px; + + .btn-remove { + @include btn-middle; + @include btn-remove; + + float: left !important; + } +} + /* * Projects list rendered on dashboard and user page */ + .projects-list { @include basic-list; @@ -306,9 +468,15 @@ pre.light-well { color: #4c4e54; } - .pull-right.light { + .project-controls { + float: right; + color: $gl-gray; line-height: 45px; color: #7f8fa4; + + a:hover { + text-decoration: none; + } } .project-description { @@ -338,3 +506,8 @@ pre.light-well { margin-top: -1px; } } + +.inline-form { + display: inline-block; +} + diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 587d09a04a5..271cc547e2b 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -78,36 +78,6 @@ } } } - - .blame { - img.avatar { - border: 0 none; - float: none; - margin: 0; - padding: 0; - } - td.blame-commit { - background: #f9f9f9; - min-width: 350px; - - .commit-author-link { - color: #888; - } - } - td.blame-numbers { - pre { - color: #AAA; - white-space: pre; - } - background: #f1f1f1; - border-left: 1px solid #DDD; - } - td.lines { - code { - font-family: $monospace_font; - } - } - } } .tree-ref-holder { @@ -132,20 +102,30 @@ list-style: none; margin: 0; padding: 0; - margin-bottom: 10px; + margin-bottom: 5px; .commit { - padding: 10px 15px; + padding: $gl-padding 0; .commit-row-title { - font-size: 13px; - .commit-row-message { font-weight: normal; - color: #555; } } } } #modal-remove-blob > .modal-dialog { width: 850px; } + +.blob-upload-dropzone-previews { + text-align: center; + border: 2px; + border-style: dashed; + border-color: $border-color; + min-height: 200px; +} + +.upload-link { + font-weight: normal; + color: $md-link-color; +} diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index f38e07af84b..5f70582cbb7 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -46,6 +46,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :gravatar_enabled, :twitter_sharing_enabled, :sign_in_text, + :help_page_text, :home_page_url, :after_sign_out_path, :max_attachment_size, @@ -55,6 +56,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :restricted_signup_domains_raw, :version_check_enabled, :user_oauth_applications, + :ci_enabled, restricted_visibility_levels: [], import_sources: [] ) diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb new file mode 100644 index 00000000000..3b070e65d0d --- /dev/null +++ b/app/controllers/admin/labels_controller.rb @@ -0,0 +1,58 @@ +class Admin::LabelsController < Admin::ApplicationController + before_action :set_label, only: [:show, :edit, :update, :destroy] + + def index + @labels = Label.templates.page(params[:page]).per(PER_PAGE) + end + + def show + end + + def new + @label = Label.new + end + + def edit + end + + def create + @label = Label.new(label_params) + @label.template = true + + if @label.save + redirect_to admin_labels_url, notice: "Label was created" + else + render :new + end + end + + def update + if @label.update(label_params) + redirect_to admin_labels_path, notice: 'label was successfully updated.' + else + render :edit + end + end + + def destroy + @label.destroy + @labels = Label.templates + + respond_to do |format| + format.html do + redirect_to(admin_labels_path, notice: 'Label was removed') + end + format.js + end + end + + private + + def set_label + @label = Label.find(params[:id]) + end + + def label_params + params[:label].permit(:title, :color) + end +end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 6092c79c254..00f41a10dd1 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -56,13 +56,19 @@ class Admin::UsersController < Admin::ApplicationController end def confirm - if user.confirm! + if user.confirm redirect_to :back, notice: "Successfully confirmed" else redirect_to :back, alert: "Error occurred. User was not confirmed" end end + def login_as + sign_in(user) + flash[:alert] = "Logged in as #{user.username}" + redirect_to root_path + end + def disable_two_factor user.disable_two_factor! redirect_to admin_user_path(user), diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index cb1cf13d34d..9b6472a7b13 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,5 @@ require 'gon' +require 'fogbugz' class ApplicationController < ActionController::Base include Gitlab::CurrentSettings @@ -20,7 +21,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception helper_method :abilities, :can?, :current_application_settings - helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :git_import_enabled? + helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled? rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) @@ -133,9 +134,6 @@ class ApplicationController < ActionController::Base def repository @repository ||= project.repository - rescue Grit::NoSuchPathError => e - log_exception(e) - nil end def authorize_project!(action) @@ -337,6 +335,10 @@ class ApplicationController < ActionController::Base current_application_settings.import_sources.include?('google_code') end + def fogbugz_import_enabled? + current_application_settings.import_sources.include?('fogbugz') + end + def git_import_enabled? current_application_settings.import_sources.include?('git') end diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 904d26a39f4..202e9da9eee 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -32,6 +32,7 @@ class AutocompleteController < ApplicationController @users ||= User.none @users = @users.search(params[:search]) if params[:search].present? @users = @users.active + @users = @users.reorder(:name) @users = @users.page(params[:page]).per(PER_PAGE) unless params[:search].present? diff --git a/app/controllers/ci/admin/application_controller.rb b/app/controllers/ci/admin/application_controller.rb new file mode 100644 index 00000000000..4ec2dc9c2cf --- /dev/null +++ b/app/controllers/ci/admin/application_controller.rb @@ -0,0 +1,10 @@ +module Ci + module Admin + class ApplicationController < Ci::ApplicationController + before_action :authenticate_user! + before_action :authenticate_admin! + + layout "ci/admin" + end + end +end diff --git a/app/controllers/ci/admin/application_settings_controller.rb b/app/controllers/ci/admin/application_settings_controller.rb new file mode 100644 index 00000000000..71e253fac67 --- /dev/null +++ b/app/controllers/ci/admin/application_settings_controller.rb @@ -0,0 +1,31 @@ +module Ci + class Admin::ApplicationSettingsController < Ci::Admin::ApplicationController + before_action :set_application_setting + + def show + end + + def update + if @application_setting.update_attributes(application_setting_params) + redirect_to ci_admin_application_settings_path, + notice: 'Application settings saved successfully' + else + render :show + end + end + + private + + def set_application_setting + @application_setting = Ci::ApplicationSetting.current + @application_setting ||= Ci::ApplicationSetting.create_from_defaults + end + + def application_setting_params + params.require(:application_setting).permit( + :all_broken_builds, + :add_pusher, + ) + end + end +end diff --git a/app/controllers/ci/admin/builds_controller.rb b/app/controllers/ci/admin/builds_controller.rb new file mode 100644 index 00000000000..38abfdeafbf --- /dev/null +++ b/app/controllers/ci/admin/builds_controller.rb @@ -0,0 +1,18 @@ +module Ci + class Admin::BuildsController < Ci::Admin::ApplicationController + def index + @scope = params[:scope] + @builds = Ci::Build.order('created_at DESC').page(params[:page]).per(30) + + @builds = + case @scope + when "pending" + @builds.pending + when "running" + @builds.running + else + @builds + end + end + end +end diff --git a/app/controllers/ci/admin/events_controller.rb b/app/controllers/ci/admin/events_controller.rb new file mode 100644 index 00000000000..5939efff980 --- /dev/null +++ b/app/controllers/ci/admin/events_controller.rb @@ -0,0 +1,9 @@ +module Ci + class Admin::EventsController < Ci::Admin::ApplicationController + EVENTS_PER_PAGE = 50 + + def index + @events = Ci::Event.admin.order('created_at DESC').page(params[:page]).per(EVENTS_PER_PAGE) + end + end +end diff --git a/app/controllers/ci/admin/projects_controller.rb b/app/controllers/ci/admin/projects_controller.rb new file mode 100644 index 00000000000..5bbd0ce7396 --- /dev/null +++ b/app/controllers/ci/admin/projects_controller.rb @@ -0,0 +1,19 @@ +module Ci + class Admin::ProjectsController < Ci::Admin::ApplicationController + def index + @projects = Ci::Project.ordered_by_last_commit_date.page(params[:page]).per(30) + end + + def destroy + project.destroy + + redirect_to ci_projects_url + end + + protected + + def project + @project ||= Ci::Project.find(params[:id]) + end + end +end diff --git a/app/controllers/ci/admin/runner_projects_controller.rb b/app/controllers/ci/admin/runner_projects_controller.rb new file mode 100644 index 00000000000..e7de6eb12ca --- /dev/null +++ b/app/controllers/ci/admin/runner_projects_controller.rb @@ -0,0 +1,34 @@ +module Ci + class Admin::RunnerProjectsController < Ci::Admin::ApplicationController + layout 'ci/project' + + def index + @runner_projects = project.runner_projects.all + @runner_project = project.runner_projects.new + end + + def create + @runner = Ci::Runner.find(params[:runner_project][:runner_id]) + + if @runner.assign_to(project, current_user) + redirect_to ci_admin_runner_path(@runner) + else + redirect_to ci_admin_runner_path(@runner), alert: 'Failed adding runner to project' + end + end + + def destroy + rp = Ci::RunnerProject.find(params[:id]) + runner = rp.runner + rp.destroy + + redirect_to ci_admin_runner_path(runner) + end + + private + + def project + @project ||= Ci::Project.find(params[:project_id]) + end + end +end diff --git a/app/controllers/ci/admin/runners_controller.rb b/app/controllers/ci/admin/runners_controller.rb new file mode 100644 index 00000000000..9a68add9083 --- /dev/null +++ b/app/controllers/ci/admin/runners_controller.rb @@ -0,0 +1,72 @@ +module Ci + class Admin::RunnersController < Ci::Admin::ApplicationController + before_action :runner, except: :index + + def index + @runners = Ci::Runner.order('id DESC') + @runners = @runners.search(params[:search]) if params[:search].present? + @runners = @runners.page(params[:page]).per(30) + @active_runners_cnt = Ci::Runner.where("contacted_at > ?", 1.minutes.ago).count + end + + def show + @builds = @runner.builds.order('id DESC').first(30) + @projects = Ci::Project.all + if params[:search].present? + @gl_projects = ::Project.search(params[:search]) + @projects = @projects.where(gitlab_id: @gl_projects.select(:id)) + end + @projects = @projects.where("ci_projects.id NOT IN (?)", @runner.projects.pluck(:id)) if @runner.projects.any? + @projects = @projects.page(params[:page]).per(30) + end + + def update + @runner.update_attributes(runner_params) + + respond_to do |format| + format.js + format.html { redirect_to ci_admin_runner_path(@runner) } + end + end + + def destroy + @runner.destroy + + redirect_to ci_admin_runners_path + end + + def resume + if @runner.update_attributes(active: true) + redirect_to ci_admin_runners_path, notice: 'Runner was successfully updated.' + else + redirect_to ci_admin_runners_path, alert: 'Runner was not updated.' + end + end + + def pause + if @runner.update_attributes(active: false) + redirect_to ci_admin_runners_path, notice: 'Runner was successfully updated.' + else + redirect_to ci_admin_runners_path, alert: 'Runner was not updated.' + end + end + + def assign_all + Ci::Project.unassigned(@runner).all.each do |project| + @runner.assign_to(project, current_user) + end + + redirect_to ci_admin_runner_path(@runner), notice: "Runner was assigned to all projects" + end + + private + + def runner + @runner ||= Ci::Runner.find(params[:id]) + end + + def runner_params + params.require(:runner).permit(:token, :description, :tag_list, :contacted_at, :active) + end + end +end diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb new file mode 100644 index 00000000000..d8227e632e4 --- /dev/null +++ b/app/controllers/ci/application_controller.rb @@ -0,0 +1,83 @@ +module Ci + class ApplicationController < ::ApplicationController + before_action :check_enable_flag! + + def self.railtie_helpers_paths + "app/helpers/ci" + end + + helper_method :gl_project + + private + + def check_enable_flag! + unless current_application_settings.ci_enabled + redirect_to(disabled_ci_projects_path) + return + end + end + + def authenticate_public_page! + unless project.public + authenticate_user! + + return access_denied! unless can?(current_user, :read_project, gl_project) + end + end + + def authenticate_token! + unless project.valid_token?(params[:token]) + return head(403) + end + end + + def authorize_access_project! + unless can?(current_user, :read_project, gl_project) + return page_404 + end + end + + def authorize_manage_builds! + unless can?(current_user, :manage_builds, gl_project) + return page_404 + end + end + + def authenticate_admin! + return render_404 unless current_user.is_admin? + end + + def authorize_manage_project! + unless can?(current_user, :admin_project, gl_project) + return page_404 + end + end + + def page_404 + render file: "#{Rails.root}/public/404.html", status: 404, layout: false + end + + def default_headers + headers['X-Frame-Options'] = 'DENY' + headers['X-XSS-Protection'] = '1; mode=block' + end + + # JSON for infinite scroll via Pager object + def pager_json(partial, count) + html = render_to_string( + partial, + layout: false, + formats: [:html] + ) + + render json: { + html: html, + count: count + } + end + + def gl_project + ::Project.find(@project.gitlab_id) + end + end +end diff --git a/app/controllers/ci/builds_controller.rb b/app/controllers/ci/builds_controller.rb new file mode 100644 index 00000000000..80ee8666792 --- /dev/null +++ b/app/controllers/ci/builds_controller.rb @@ -0,0 +1,78 @@ +module Ci + class BuildsController < Ci::ApplicationController + before_action :authenticate_user!, except: [:status, :show] + before_action :authenticate_public_page!, only: :show + before_action :project + before_action :authorize_access_project!, except: [:status, :show] + before_action :authorize_manage_project!, except: [:status, :show, :retry, :cancel] + before_action :authorize_manage_builds!, only: [:retry, :cancel] + before_action :build, except: [:show] + layout 'ci/build' + + def show + if params[:id] =~ /\A\d+\Z/ + @build = build + else + # try to find commit by sha + commit = commit_by_sha + + if commit + # Redirect to commit page + redirect_to ci_project_ref_commit_path(@project, @build.commit.ref, @build.commit.sha) + return + end + end + + raise ActiveRecord::RecordNotFound unless @build + + @builds = @project.commits.find_by_sha(@build.sha).builds.order('id DESC') + @builds = @builds.where("id not in (?)", @build.id).page(params[:page]).per(20) + @commit = @build.commit + + respond_to do |format| + format.html + format.json do + render json: @build.to_json(methods: :trace_html) + end + end + end + + def retry + if @build.commands.blank? + return page_404 + end + + build = Ci::Build.retry(@build) + + if params[:return_to] + redirect_to URI.parse(params[:return_to]).path + else + redirect_to ci_project_build_path(project, build) + end + end + + def status + render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha) + end + + def cancel + @build.cancel + + redirect_to ci_project_build_path(@project, @build) + end + + protected + + def project + @project = Ci::Project.find(params[:project_id]) + end + + def build + @build ||= project.builds.unscoped.find_by(id: params[:id]) + end + + def commit_by_sha + @project.commits.find_by(sha: params[:id]) + end + end +end diff --git a/app/controllers/ci/commits_controller.rb b/app/controllers/ci/commits_controller.rb new file mode 100644 index 00000000000..7a0a500fbe6 --- /dev/null +++ b/app/controllers/ci/commits_controller.rb @@ -0,0 +1,38 @@ +module Ci + class CommitsController < Ci::ApplicationController + before_action :authenticate_user!, except: [:status, :show] + before_action :authenticate_public_page!, only: :show + before_action :project + before_action :authorize_access_project!, except: [:status, :show, :cancel] + before_action :authorize_manage_builds!, only: [:cancel] + before_action :commit, only: :show + layout 'ci/commit' + + def show + @builds = @commit.builds + end + + def status + commit = Ci::Project.find(params[:project_id]).commits.find_by_sha_and_ref!(params[:id], params[:ref_id]) + render json: commit.to_json(only: [:id, :sha], methods: [:status, :coverage]) + rescue ActiveRecord::RecordNotFound + render json: { status: "not_found" } + end + + def cancel + commit.builds.running_or_pending.each(&:cancel) + + redirect_to ci_project_ref_commits_path(project, commit.ref, commit.sha) + end + + private + + def project + @project ||= Ci::Project.find(params[:project_id]) + end + + def commit + @commit ||= Ci::Project.find(params[:project_id]).commits.find_by_sha_and_ref!(params[:id], params[:ref_id]) + end + end +end diff --git a/app/controllers/ci/events_controller.rb b/app/controllers/ci/events_controller.rb new file mode 100644 index 00000000000..89b784a1e89 --- /dev/null +++ b/app/controllers/ci/events_controller.rb @@ -0,0 +1,21 @@ +module Ci + class EventsController < Ci::ApplicationController + EVENTS_PER_PAGE = 50 + + before_action :authenticate_user! + before_action :project + before_action :authorize_manage_project! + + layout 'ci/project' + + def index + @events = project.events.order("created_at DESC").page(params[:page]).per(EVENTS_PER_PAGE) + end + + private + + def project + @project ||= Ci::Project.find(params[:project_id]) + end + end +end diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb new file mode 100644 index 00000000000..a81e4e319ff --- /dev/null +++ b/app/controllers/ci/lints_controller.rb @@ -0,0 +1,26 @@ +module Ci + class LintsController < Ci::ApplicationController + before_action :authenticate_user! + + def show + end + + def create + if params[:content].blank? + @status = false + @error = "Please provide content of .gitlab-ci.yml" + else + @config_processor = Ci::GitlabCiYamlProcessor.new params[:content] + @stages = @config_processor.stages + @builds = @config_processor.builds + @status = true + end + rescue Ci::GitlabCiYamlProcessor::ValidationError => e + @error = e.message + @status = false + rescue Exception => e + @error = "Undefined error" + @status = false + end + end +end diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb new file mode 100644 index 00000000000..e8788955eba --- /dev/null +++ b/app/controllers/ci/projects_controller.rb @@ -0,0 +1,56 @@ +module Ci + class ProjectsController < Ci::ApplicationController + before_action :authenticate_user!, except: [:build, :badge, :show] + before_action :authenticate_public_page!, only: :show + before_action :project, only: [:build, :show, :badge, :toggle_shared_runners, :dumped_yaml] + before_action :authorize_access_project!, except: [:build, :badge, :show, :new, :disabled] + before_action :authorize_manage_project!, only: [:toggle_shared_runners, :dumped_yaml] + before_action :authenticate_token!, only: [:build] + before_action :no_cache, only: [:badge] + skip_before_action :check_enable_flag!, only: [:disabled] + protect_from_forgery except: :build + + layout 'ci/project', except: [:index, :disabled] + + def disabled + end + + def show + @ref = params[:ref] + + @commits = @project.commits.reverse_order + @commits = @commits.where(ref: @ref) if @ref + @commits = @commits.page(params[:page]).per(20) + end + + # Project status badge + # Image with build status for sha or ref + def badge + image = Ci::ImageForBuildService.new.execute(@project, params) + + send_file image.path, filename: image.name, disposition: 'inline', type:"image/svg+xml" + end + + def toggle_shared_runners + project.toggle!(:shared_runners_enabled) + + redirect_to namespace_project_runners_path(project.gl_project.namespace, project.gl_project) + end + + def dumped_yaml + send_data @project.generated_yaml_config, filename: '.gitlab-ci.yml' + end + + protected + + def project + @project ||= Ci::Project.find(params[:id]) + end + + def no_cache + response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate" + response.headers["Pragma"] = "no-cache" + response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" + end + end +end diff --git a/app/controllers/ci/runner_projects_controller.rb b/app/controllers/ci/runner_projects_controller.rb new file mode 100644 index 00000000000..97f01d40af5 --- /dev/null +++ b/app/controllers/ci/runner_projects_controller.rb @@ -0,0 +1,36 @@ +module Ci + class RunnerProjectsController < Ci::ApplicationController + before_action :authenticate_user! + before_action :project + before_action :authorize_manage_project! + + layout 'ci/project' + + def create + @runner = Ci::Runner.find(params[:runner_project][:runner_id]) + + return head(403) unless current_user.ci_authorized_runners.include?(@runner) + + path = runners_path(@project.gl_project) + + if @runner.assign_to(project, current_user) + redirect_to path + else + redirect_to path, alert: 'Failed adding runner to project' + end + end + + def destroy + runner_project = project.runner_projects.find(params[:id]) + runner_project.destroy + + redirect_to runners_path(@project.gl_project) + end + + private + + def project + @project ||= Ci::Project.find(params[:project_id]) + end + end +end diff --git a/app/controllers/ci/services_controller.rb b/app/controllers/ci/services_controller.rb new file mode 100644 index 00000000000..52c96a34ce8 --- /dev/null +++ b/app/controllers/ci/services_controller.rb @@ -0,0 +1,59 @@ +module Ci + class ServicesController < Ci::ApplicationController + before_action :authenticate_user! + before_action :project + before_action :authorize_access_project! + before_action :authorize_manage_project! + before_action :service, only: [:edit, :update, :test] + + respond_to :html + + layout 'ci/project' + + def index + @project.build_missing_services + @services = @project.services.reload + end + + def edit + end + + def update + if @service.update_attributes(service_params) + redirect_to edit_ci_project_service_path(@project, @service.to_param) + else + render 'edit' + end + end + + def test + last_build = @project.builds.last + + if @service.execute(last_build) + message = { notice: 'We successfully tested the service' } + else + message = { alert: 'We tried to test the service but error occurred' } + end + + redirect_to :back, message + end + + private + + def project + @project = Ci::Project.find(params[:project_id]) + end + + def service + @service ||= @project.services.find { |service| service.to_param == params[:id] } + end + + def service_params + params.require(:service).permit( + :type, :active, :webhook, :notify_only_broken_builds, + :email_recipients, :email_only_broken_builds, :email_add_pusher, + :hipchat_token, :hipchat_room, :hipchat_server + ) + end + end +end diff --git a/app/controllers/ci/web_hooks_controller.rb b/app/controllers/ci/web_hooks_controller.rb new file mode 100644 index 00000000000..24074a6d9ac --- /dev/null +++ b/app/controllers/ci/web_hooks_controller.rb @@ -0,0 +1,53 @@ +module Ci + class WebHooksController < Ci::ApplicationController + before_action :authenticate_user! + before_action :project + before_action :authorize_access_project! + before_action :authorize_manage_project! + + layout 'ci/project' + + def index + @web_hooks = @project.web_hooks + @web_hook = Ci::WebHook.new + end + + def create + @web_hook = @project.web_hooks.new(web_hook_params) + @web_hook.save + + if @web_hook.valid? + redirect_to ci_project_web_hooks_path(@project) + else + @web_hooks = @project.web_hooks.select(&:persisted?) + render :index + end + end + + def test + Ci::TestHookService.new.execute(hook, current_user) + + redirect_to :back + end + + def destroy + hook.destroy + + redirect_to ci_project_web_hooks_path(@project) + end + + private + + def hook + @web_hook ||= @project.web_hooks.find(params[:id]) + end + + def project + @project = Ci::Project.find(params[:project_id]) + end + + def web_hook_params + params.require(:web_hook).permit(:url) + end + end +end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index da96171e885..467d0f81aca 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -1,6 +1,21 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController before_action :event_filter + def index + @projects = current_user.authorized_projects.sorted_by_activity.non_archived + @projects = @projects.includes(:namespace) + @last_push = current_user.recent_push + + respond_to do |format| + format.html + format.atom do + event_filter + load_events + render layout: false + end + end + end + def starred @projects = current_user.starred_projects @projects = @projects.includes(:namespace, :forked_from_project, :tags) diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb new file mode 100644 index 00000000000..f4354c6d8ca --- /dev/null +++ b/app/controllers/dashboard/snippets_controller.rb @@ -0,0 +1,10 @@ +class Dashboard::SnippetsController < Dashboard::ApplicationController + def index + @snippets = SnippetsFinder.new.execute(current_user, + filter: :by_user, + user: current_user, + scope: params[:scope] + ) + @snippets = @snippets.page(params[:page]).per(PER_PAGE) + end +end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 2bc2e5e58f5..4ebb3d7276e 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,23 +1,8 @@ class DashboardController < Dashboard::ApplicationController - before_action :load_projects, except: :activity before_action :event_filter, only: :activity respond_to :html - def show - @projects = @projects.includes(:namespace) - @last_push = current_user.recent_push - - respond_to do |format| - format.html - format.atom do - event_filter - load_events - render layout: false - end - end - end - def merge_requests @merge_requests = get_merge_requests_collection @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE) @@ -50,10 +35,6 @@ class DashboardController < Dashboard::ApplicationController protected - def load_projects - @projects = current_user.authorized_projects.sorted_by_activity.non_archived - end - def load_events project_ids = if params[:filter] == "starred" diff --git a/app/controllers/explore/application_controller.rb b/app/controllers/explore/application_controller.rb index 4b275033d26..461fc059a3c 100644 --- a/app/controllers/explore/application_controller.rb +++ b/app/controllers/explore/application_controller.rb @@ -1,3 +1,5 @@ class Explore::ApplicationController < ApplicationController + skip_before_action :authenticate_user!, :reject_blocked + layout 'explore' end diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb index 55cda0cff17..9575a87ee41 100644 --- a/app/controllers/explore/groups_controller.rb +++ b/app/controllers/explore/groups_controller.rb @@ -1,7 +1,4 @@ class Explore::GroupsController < Explore::ApplicationController - skip_before_action :authenticate_user!, - :reject_blocked, :set_current_user_for_observers - def index @groups = GroupsFinder.new.execute(current_user) @groups = @groups.search(params[:search]) if params[:search].present? diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 6c733c1ae4d..a5aeaed66c5 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -1,7 +1,4 @@ class Explore::ProjectsController < Explore::ApplicationController - skip_before_action :authenticate_user!, - :reject_blocked - def index @projects = ProjectsFinder.new.execute(current_user) @tags = @projects.tags_on(:tags) diff --git a/app/controllers/explore/snippets_controller.rb b/app/controllers/explore/snippets_controller.rb new file mode 100644 index 00000000000..b70ac51d06e --- /dev/null +++ b/app/controllers/explore/snippets_controller.rb @@ -0,0 +1,6 @@ +class Explore::SnippetsController < Explore::ApplicationController + def index + @snippets = SnippetsFinder.new.execute(current_user, filter: :all) + @snippets = @snippets.page(params[:page]).per(PER_PAGE) + end +end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 279c6ef0f4d..524218290c6 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -4,7 +4,7 @@ class GroupsController < Groups::ApplicationController before_action :group, except: [:new, :create] # Authorize - before_action :authorize_read_group!, except: [:new, :create] + before_action :authorize_read_group!, except: [:show, :new, :create] before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects] before_action :authorize_create_group!, only: [:new, :create] @@ -14,6 +14,10 @@ class GroupsController < Groups::ApplicationController layout :determine_layout + def index + redirect_to(current_user ? dashboard_groups_path : explore_groups_path) + end + def new @group = Group.new end diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 71831c5380d..55050615473 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -1,7 +1,14 @@ class HelpController < ApplicationController + skip_before_action :authenticate_user!, :reject_blocked + layout 'help' def index + @help_index = File.read(Rails.root.join('doc', 'README.md')) + + # Prefix Markdown links with `help/` unless they already have been + # See http://rubular.com/r/nwwhzH6Z8X + @help_index.gsub!(/(\]\()(?!help\/)([^\)\(]+)(\))/, '\1help/\2\3') end def show diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb new file mode 100644 index 00000000000..849646cd665 --- /dev/null +++ b/app/controllers/import/fogbugz_controller.rb @@ -0,0 +1,104 @@ +class Import::FogbugzController < Import::BaseController + before_action :verify_fogbugz_import_enabled + before_action :user_map, only: [:new_user_map, :create_user_map] + + rescue_from Fogbugz::AuthenticationException, with: :fogbugz_unauthorized + + def new + + end + + def callback + begin + res = Gitlab::FogbugzImport::Client.new(import_params.symbolize_keys) + rescue + # If the URI is invalid various errors can occur + return redirect_to new_import_fogbugz_path, alert: 'Could not connect to FogBugz, check your URL' + end + session[:fogbugz_token] = res.get_token + session[:fogbugz_uri] = params[:uri] + + redirect_to new_user_map_import_fogbugz_path + end + + def new_user_map + + end + + def create_user_map + user_map = params[:users] + + unless user_map.is_a?(Hash) && user_map.all? { |k, v| !v[:name].blank? } + flash.now[:alert] = 'All users must have a name.' + + render 'new_user_map' and return + end + + session[:fogbugz_user_map] = user_map + + flash[:notice] = 'The user map has been saved. Continue by selecting the projects you want to import.' + + redirect_to status_import_fogbugz_path + end + + def status + unless client.valid? + return redirect_to new_import_fogbugz_path + end + + @repos = client.repos + + @already_added_projects = current_user.created_projects.where(import_type: 'fogbugz') + already_added_projects_names = @already_added_projects.pluck(:import_source) + + @repos.reject! { |repo| already_added_projects_names.include? repo.name } + end + + def jobs + jobs = current_user.created_projects.where(import_type: 'fogbugz').to_json(only: [:id, :import_status]) + render json: jobs + end + + def create + @repo_id = params[:repo_id] + repo = client.repo(@repo_id) + fb_session = { uri: session[:fogbugz_uri], token: session[:fogbugz_token] } + @target_namespace = current_user.namespace + @project_name = repo.name + + namespace = @target_namespace + + umap = session[:fogbugz_user_map] || client.user_map + + @project = Gitlab::FogbugzImport::ProjectCreator.new(repo, fb_session, namespace, current_user, umap).execute + end + + private + + def client + @client ||= Gitlab::FogbugzImport::Client.new(token: session[:fogbugz_token], uri: session[:fogbugz_uri]) + end + + def user_map + @user_map ||= begin + user_map = client.user_map + + stored_user_map = session[:fogbugz_user_map] + user_map.update(stored_user_map) if stored_user_map + + user_map + end + end + + def fogbugz_unauthorized(exception) + redirect_to new_import_fogbugz_path, alert: exception.message + end + + def import_params + params.permit(:uri, :email, :password) + end + + def verify_fogbugz_import_enabled + not_found! unless fogbugz_import_enabled? + end +end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index eb3c8233530..8ef10a17f55 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -24,7 +24,7 @@ class InvitesController < ApplicationController path = if current_user - dashboard_path + dashboard_projects_path else new_user_session_path end @@ -73,7 +73,7 @@ class InvitesController < ApplicationController path = group_path(group) else label = "who knows what" - path = dashboard_path + path = dashboard_projects_path end [label, path] diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb index 83eec1bf4a2..282012c60a1 100644 --- a/app/controllers/namespaces_controller.rb +++ b/app/controllers/namespaces_controller.rb @@ -14,7 +14,7 @@ class NamespacesController < ApplicationController if user redirect_to user_path(user) - elsif group && can?(current_user, :read_group, group) + elsif group redirect_to group_path(group) elsif current_user.nil? authenticate_user! diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index fc31118124b..dc22101cd5e 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -1,7 +1,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController include Gitlab::CurrentSettings include PageLayoutHelper - + before_action :verify_user_oauth_applications_enabled before_action :authenticate_user! diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index f9af0871cf1..e6b99be37fb 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -9,7 +9,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController end def create - if current_user.valid_otp?(params[:pin_code]) + if current_user.validate_and_consume_otp!(params[:pin_code]) current_user.two_factor_enabled = true @codes = current_user.generate_otp_backup_codes! current_user.save! diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index ee88d49b400..519d6d6127e 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -25,4 +25,14 @@ class Projects::ApplicationController < ApplicationController ) end end + + private + + def ci_enabled + return render_404 unless @project.gitlab_ci? + end + + def ci_project + @ci_project ||= @project.ensure_gitlab_ci_project + end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 100d3d3b317..8776721d243 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -26,10 +26,16 @@ class Projects::BlobController < Projects::ApplicationController if result[:status] == :success flash[:notice] = "Your changes have been successfully committed" - redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) + respond_to do |format| + format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) } + format.json { render json: { message: "success", filePath: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) } } + end else flash[:alert] = result[:message] - render :new + respond_to do |format| + format.html { render :new } + format.json { render json: { message: "failed", filePath: namespace_project_new_blob_path(@project.namespace, @project, @id) } } + end end end @@ -45,10 +51,16 @@ class Projects::BlobController < Projects::ApplicationController if result[:status] == :success flash[:notice] = "Your changes have been successfully committed" - redirect_to after_edit_path + respond_to do |format| + format.html { redirect_to after_edit_path } + format.json { render json: { message: "success", filePath: after_edit_path } } + end else flash[:alert] = result[:message] - render :edit + respond_to do |format| + format.html { render :edit } + format.json { render json: { message: "failed", filePath: namespace_project_new_blob_path(@project.namespace, @project, @id) } } + end end end @@ -146,11 +158,19 @@ class Projects::BlobController < Projects::ApplicationController @file_path = if action_name.to_s == 'create' + if params[:file].present? + params[:file_name] = params[:file].original_filename + end File.join(@path, File.basename(params[:file_name])) else @path end + if params[:file].present? + params[:content] = Base64.encode64(params[:file].read) + params[:encoding] = 'base64' + end + @commit_params = { file_path: @file_path, current_branch: @current_branch, diff --git a/app/controllers/projects/ci_settings_controller.rb b/app/controllers/projects/ci_settings_controller.rb new file mode 100644 index 00000000000..a263242a850 --- /dev/null +++ b/app/controllers/projects/ci_settings_controller.rb @@ -0,0 +1,36 @@ +class Projects::CiSettingsController < Projects::ApplicationController + before_action :ci_project + before_action :authorize_admin_project! + + layout "project_settings" + + def edit + end + + def update + if ci_project.update_attributes(project_params) + Ci::EventService.new.change_project_settings(current_user, ci_project) + + redirect_to edit_namespace_project_ci_settings_path(project.namespace, project), notice: 'Project was successfully updated.' + else + render action: "edit" + end + end + + def destroy + ci_project.destroy + Ci::EventService.new.remove_project(current_user, ci_project) + project.gitlab_ci_service.update_attributes(active: false) + + redirect_to project_path(project), notice: "CI was disabled for this project" + end + + protected + + def project_params + params.require(:project).permit(:path, :timeout, :timeout_in_minutes, :default_ref, :always_build, + :polling_interval, :public, :ssh_url_to_repo, :allow_git_fetch, :email_recipients, + :email_add_pusher, :email_only_broken_builds, :coverage_regex, :shared_runners_enabled, :token, + { variables_attributes: [:id, :key, :value, :_destroy] }) + end +end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 78d42d695b6..2fae5057138 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -22,6 +22,8 @@ class Projects::CommitController < Projects::ApplicationController commit_id: @commit.id } + @ci_commit = project.ci_commit(commit.sha) + respond_to do |format| format.html format.diff { render text: @commit.to_diff } diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index d9b3adae95b..d15004f93a6 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -16,10 +16,12 @@ class Projects::CompareController < Projects::ApplicationController compare_result = CompareService.new. execute(@project, head_ref, @project, base_ref) - @commits = compare_result.commits - @diffs = compare_result.diffs - @commit = @commits.last - @line_notes = [] + if compare_result + @commits = compare_result.commits + @diffs = compare_result.diffs + @commit = @commits.last + @line_notes = [] + end end def create diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index 9e72597ea87..8a785076bb7 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -13,10 +13,14 @@ class Projects::ForksController < Projects::ApplicationController @forked_project = ::Projects::ForkService.new(project, current_user, namespace: namespace).execute if @forked_project.saved? && @forked_project.forked? - redirect_to( - namespace_project_path(@forked_project.namespace, @forked_project), - notice: 'Project was successfully forked.' - ) + if @forked_project.import_in_progress? + redirect_to namespace_project_import_path(@forked_project.namespace, @forked_project) + else + redirect_to( + namespace_project_path(@forked_project.namespace, @forked_project), + notice: 'Project was successfully forked.' + ) + end else render :error end diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 0b6f7f5c91e..418b92040bc 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -5,6 +5,7 @@ class Projects::GraphsController < Projects::ApplicationController before_action :require_non_empty_project before_action :assign_ref_vars before_action :authorize_download_code! + before_action :ci_enabled, only: :ci def show respond_to do |format| @@ -23,6 +24,16 @@ class Projects::GraphsController < Projects::ApplicationController @commits_per_month = @commits_graph.commits_per_month end + def ci + ci_project = @project.gitlab_ci_project + + @charts = {} + @charts[:week] = Ci::Charts::WeekChart.new(ci_project) + @charts[:month] = Ci::Charts::MonthChart.new(ci_project) + @charts[:year] = Ci::Charts::YearChart.new(ci_project) + @charts[:build_times] = Ci::Charts::BuildTime.new(ci_project) + end + private def fetch_graph diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index f3054881daf..7574842cd43 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -7,6 +7,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits] before_action :validates_merge_request, only: [:show, :diffs, :commits] before_action :define_show_vars, only: [:show, :diffs, :commits] + before_action :ensure_ref_fetched, only: [:show, :commits, :diffs] # Allow read any merge_request before_action :authorize_read_merge_request! @@ -257,8 +258,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commits = @merge_request.commits @merge_request_diff = @merge_request.merge_request_diff - @source_branch = @merge_request.source_project.repository.find_branch(@merge_request.source_branch).try(:name) - + if @merge_request.locked_long_ago? @merge_request.unlock_mr @merge_request.close @@ -277,4 +277,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController :state_event, :description, :task_num, label_ids: [] ) end + + # Make sure merge requests created before 8.0 + # have head file in refs/merge-requests/ + def ensure_ref_fetched + @merge_request.ensure_ref_fetched + end end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 9efe9704d1e..86f4a02a6e9 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -66,12 +66,7 @@ class Projects::MilestonesController < Projects::ApplicationController def destroy return access_denied! unless can?(current_user, :admin_milestone, @project) - update_params = { milestone: nil } - @milestone.issues.each do |issue| - Issues::UpdateService.new(@project, current_user, update_params).execute(issue) - end - - @milestone.destroy + Milestones::DestroyService.new(project, current_user).execute(milestone) respond_to do |format| format.html { redirect_to namespace_project_milestones_path } diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index b82b6f45d59..cf73bc01c8f 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -78,7 +78,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController @project.project_members.find_by(user_id: current_user).destroy respond_to do |format| - format.html { redirect_to dashboard_path } + format.html { redirect_to dashboard_projects_path } format.js { render nothing: true } end end diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb new file mode 100644 index 00000000000..6cb6e3ef6d4 --- /dev/null +++ b/app/controllers/projects/runners_controller.rb @@ -0,0 +1,65 @@ +class Projects::RunnersController < Projects::ApplicationController + before_action :ci_project + before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show] + before_action :authorize_admin_project! + + layout 'project_settings' + + def index + @runners = @ci_project.runners.order('id DESC') + @specific_runners = + Ci::Runner.specific.includes(:runner_projects). + where(Ci::RunnerProject.table_name => { project_id: current_user.authorized_projects } ). + where.not(id: @runners).order("#{Ci::Runner.table_name}.id DESC").page(params[:page]).per(20) + @shared_runners = Ci::Runner.shared.active + @shared_runners_count = @shared_runners.count(:all) + end + + def edit + end + + def update + if @runner.update_attributes(runner_params) + redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' + else + redirect_to runner_path(@runner), alert: 'Runner was not updated.' + end + end + + def destroy + if @runner.only_for?(@ci_project) + @runner.destroy + end + + redirect_to runners_path(@project) + end + + def resume + if @runner.update_attributes(active: true) + redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' + else + redirect_to runner_path(@runner), alert: 'Runner was not updated.' + end + end + + def pause + if @runner.update_attributes(active: false) + redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' + else + redirect_to runner_path(@runner), alert: 'Runner was not updated.' + end + end + + def show + end + + protected + + def set_runner + @runner ||= @ci_project.runners.find(params[:id]) + end + + def runner_params + params.require(:runner).permit(:description, :tag_list, :contacted_at, :active) + end +end diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb new file mode 100644 index 00000000000..782ebd01b05 --- /dev/null +++ b/app/controllers/projects/triggers_controller.rb @@ -0,0 +1,35 @@ +class Projects::TriggersController < Projects::ApplicationController + before_action :ci_project + before_action :authorize_admin_project! + + layout 'project_settings' + + def index + @triggers = @ci_project.triggers + @trigger = Ci::Trigger.new + end + + def create + @trigger = @ci_project.triggers.new + @trigger.save + + if @trigger.valid? + redirect_to namespace_project_triggers_path(@project.namespace, @project) + else + @triggers = @ci_project.triggers.select(&:persisted?) + render :index + end + end + + def destroy + trigger.destroy + + redirect_to namespace_project_triggers_path(@project.namespace, @project) + end + + private + + def trigger + @trigger ||= @ci_project.triggers.find(params[:id]) + end +end diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb new file mode 100644 index 00000000000..d6561a45a70 --- /dev/null +++ b/app/controllers/projects/variables_controller.rb @@ -0,0 +1,25 @@ +class Projects::VariablesController < Projects::ApplicationController + before_action :ci_project + before_action :authorize_admin_project! + + layout 'project_settings' + + def show + end + + def update + if ci_project.update_attributes(project_params) + Ci::EventService.new.change_project_settings(current_user, ci_project) + + redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variables were successfully updated.' + else + render action: 'show' + end + end + + private + + def project_params + params.require(:project).permit({ variables_attributes: [:id, :key, :value, :_destroy] }) + end +end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index dafc11d0707..213c2a7173b 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -10,6 +10,10 @@ class ProjectsController < ApplicationController layout :determine_layout + def index + redirect_to(current_user ? root_path : explore_root_path) + end + def new @project = Project.new end @@ -82,6 +86,10 @@ class ProjectsController < ApplicationController if @project.empty_repo? render 'projects/empty' else + if current_user + @membership = @project.project_member_by_id(current_user.id) + end + render :show end else @@ -105,7 +113,7 @@ class ProjectsController < ApplicationController if request.referer.include?('/admin') redirect_to admin_namespaces_projects_path else - redirect_to dashboard_path + redirect_to dashboard_projects_path end rescue Projects::DestroyService::DestroyError => ex redirect_to edit_project_path(@project), alert: ex.message diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb index fdfe00dc135..ad04c646e1b 100644 --- a/app/controllers/root_controller.rb +++ b/app/controllers/root_controller.rb @@ -6,10 +6,10 @@ # # For users who haven't customized the setting, we simply delegate to # `DashboardController#show`, which is the default. -class RootController < DashboardController - before_action :redirect_to_custom_dashboard, only: [:show] +class RootController < Dashboard::ProjectsController + before_action :redirect_to_custom_dashboard, only: [:index] - def show + def index super end @@ -20,7 +20,12 @@ class RootController < DashboardController case current_user.dashboard when 'stars' + flash.keep redirect_to starred_dashboard_projects_path + when 'project_activity' + redirect_to activity_dashboard_path + when 'starred_project_activity' + redirect_to activity_dashboard_path(filter: 'starred') else return end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index cfa565cd03e..1b60d3e27d0 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -99,7 +99,7 @@ class SessionsController < Devise::SessionsController end def valid_otp_attempt?(user) - user.valid_otp?(user_params[:otp_attempt]) || + user.validate_and_consume_otp!(user_params[:otp_attempt]) || user.invalidate_otp_backup_code!(user_params[:otp_attempt]) end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 8e7e45c781f..9f9f9a92f11 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -24,13 +24,9 @@ class SnippetsController < ApplicationController scope: params[:scope] }). page(params[:page]).per(PER_PAGE) - if @user == current_user - render 'current_user_index' - else - render 'user_index' - end + render 'index' else - @snippets = SnippetsFinder.new.execute(current_user, filter: :all).page(params[:page]).per(PER_PAGE) + redirect_to(current_user ? dashboard_snippets_path : explore_snippets_path) end end diff --git a/app/finders/trending_projects_finder.rb b/app/finders/trending_projects_finder.rb index f3f4d461efa..9ea342cb26d 100644 --- a/app/finders/trending_projects_finder.rb +++ b/app/finders/trending_projects_finder.rb @@ -2,21 +2,12 @@ class TrendingProjectsFinder def execute(current_user, start_date = nil) start_date ||= Date.today - 1.month + projects = projects_for(current_user) + # Determine trending projects based on comments count # for period of time - ex. month - trending_project_ids = Note. - select("notes.project_id, count(notes.project_id) as pcount"). - where('notes.created_at > ?', start_date). - group("project_id"). - reorder("pcount DESC"). - map(&:project_id) - - sql_order_ids = trending_project_ids.reverse. - map { |project_id| "id = #{project_id}" }.join(", ") - - # Get list of projects that user allowed to see - projects = projects_for(current_user) - projects.where(id: trending_project_ids).reorder(sql_order_ids) + projects.joins(:notes).where('notes.created_at > ?', start_date). + group("projects.id").reorder("count(notes.id) DESC") end private diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a803b66c502..39ab83ccf12 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -13,7 +13,9 @@ module ApplicationHelper # current_controller?(:commits) # => false # current_controller?(:commits, :tree) # => true def current_controller?(*args) - args.any? { |v| v.to_s.downcase == controller.controller_name } + args.any? do |v| + v.to_s.downcase == controller.controller_name || v.to_s.downcase == controller.controller_path + end end # Check if a particular action is the current one @@ -82,7 +84,7 @@ module ApplicationHelper end def default_avatar - image_path('no_avatar.png') + 'no_avatar.png' end def last_commit(project) @@ -201,7 +203,7 @@ module ApplicationHelper class: "#{html_class} js-timeago", datetime: time.getutc.iso8601, title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'), - data: { toggle: 'tooltip', placement: placement } + data: { toggle: 'tooltip', placement: placement, container: 'body' } element += javascript_tag "$('.js-timeago').timeago()" unless skip_js diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index d9502181c4f..cd99a232403 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -1,6 +1,6 @@ module AuthHelper PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2).freeze - FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos', 'crowd'].freeze + FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze def ldap_enabled? Gitlab.config.ldap.enabled @@ -40,7 +40,7 @@ module AuthHelper if provider_has_icon?(provider) file_name = "#{provider.to_s.split('_').first}_#{size}.png" - image_tag(image_path("auth_buttons/#{file_name}"), alt: label, title: "Sign in with #{label}") + image_tag("auth_buttons/#{file_name}", alt: label, title: "Sign in with #{label}") else label end diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb new file mode 100644 index 00000000000..b6658e52d09 --- /dev/null +++ b/app/helpers/builds_helper.rb @@ -0,0 +1,17 @@ +module BuildsHelper + def build_ref_link build + gitlab_ref_link build.project, build.ref + end + + def build_compare_link build + gitlab_compare_link build.project, build.commit.short_before_sha, build.short_sha + end + + def build_commit_link build + gitlab_commit_link build.project, build.short_sha + end + + def build_url(build) + ci_project_build_url(build.project, build) + end +end diff --git a/app/helpers/ci/commits_helper.rb b/app/helpers/ci/commits_helper.rb new file mode 100644 index 00000000000..9069aed5b4d --- /dev/null +++ b/app/helpers/ci/commits_helper.rb @@ -0,0 +1,24 @@ +module Ci + module CommitsHelper + def ci_commit_path(commit) + ci_project_ref_commits_path(commit.project, commit.ref, commit.sha) + end + + def commit_link(commit) + link_to(commit.short_sha, ci_commit_path(commit)) + end + + def truncate_first_line(message, length = 50) + truncate(message.each_line.first.chomp, length: length) if message + end + + def ci_commit_title(commit) + content_tag :span do + link_to( + simple_sanitize(commit.project.name), ci_project_path(commit.project) + ) + ' @ ' + + gitlab_commit_link(@project, @commit.sha) + end + end + end +end diff --git a/app/helpers/ci/gitlab_helper.rb b/app/helpers/ci/gitlab_helper.rb new file mode 100644 index 00000000000..13e4d0fd9c3 --- /dev/null +++ b/app/helpers/ci/gitlab_helper.rb @@ -0,0 +1,36 @@ +module Ci + module GitlabHelper + def no_turbolink + { :"data-no-turbolink" => "data-no-turbolink" } + end + + def gitlab_ref_link project, ref + gitlab_url = project.gitlab_url.dup + gitlab_url << "/commits/#{ref}" + link_to ref, gitlab_url, no_turbolink + end + + def gitlab_compare_link project, before, after + gitlab_url = project.gitlab_url.dup + gitlab_url << "/compare/#{before}...#{after}" + + link_to "#{before}...#{after}", gitlab_url, no_turbolink + end + + def gitlab_commit_link project, sha + gitlab_url = project.gitlab_url.dup + gitlab_url << "/commit/#{sha}" + link_to Ci::Commit.truncate_sha(sha), gitlab_url, no_turbolink + end + + def yaml_web_editor_link(project) + commits = project.commits + + if commits.any? && commits.last.push_data[:ci_yaml_file] + "#{project.gitlab_url}/edit/master/.gitlab-ci.yml" + else + "#{project.gitlab_url}/new/master" + end + end + end +end diff --git a/app/helpers/ci/projects_helper.rb b/app/helpers/ci/projects_helper.rb new file mode 100644 index 00000000000..fd991a4165a --- /dev/null +++ b/app/helpers/ci/projects_helper.rb @@ -0,0 +1,36 @@ +module Ci + module ProjectsHelper + def ref_tab_class ref = nil + 'active' if ref == @ref + end + + def success_ratio(success_builds, failed_builds) + failed_builds = failed_builds.count(:all) + success_builds = success_builds.count(:all) + + return 100 if failed_builds.zero? + + ratio = (success_builds.to_f / (success_builds + failed_builds)) * 100 + ratio.to_i + end + + def markdown_badge_code(project, ref) + url = status_ci_project_url(project, ref: ref, format: 'png') + "[](#{ci_project_url(project, ref: ref)})" + end + + def html_badge_code(project, ref) + url = status_ci_project_url(project, ref: ref, format: 'png') + "<a href='#{ci_project_url(project, ref: ref)}'><img src='#{url}' /></a>" + end + + def project_uses_specific_runner?(project) + project.runners.any? + end + + def no_runners_for_project?(project) + project.runners.blank? && + Ci::Runner.shared.blank? + end + end +end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb new file mode 100644 index 00000000000..3a88ed7107e --- /dev/null +++ b/app/helpers/ci_status_helper.rb @@ -0,0 +1,44 @@ +module CiStatusHelper + def ci_status_path(ci_commit) + ci_project_ref_commits_path(ci_commit.project, ci_commit.ref, ci_commit) + end + + def ci_status_icon(ci_commit) + ci_icon_for_status(ci_commit.status) + end + + def ci_status_color(ci_commit) + case ci_commit.status + when 'success' + 'green' + when 'failed' + 'red' + when 'running', 'pending' + 'yellow' + else + 'gray' + end + end + + def ci_status_with_icon(status) + content_tag :span, class: "ci-status ci-#{status}" do + ci_icon_for_status(status) + ' '.html_safe + status + end + end + + def ci_icon_for_status(status) + icon_name = + case status + when 'success' + 'check' + when 'failed' + 'close' + when 'running', 'pending' + 'clock-o' + else + 'circle' + end + + icon(icon_name) + end +end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index d13d80be293..9df20c9fce5 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -135,7 +135,7 @@ module CommitsHelper # size: size of the avatar image in px def commit_person_link(commit, options = {}) user = commit.send(options[:source]) - + source_name = clean(commit.send "#{options[:source]}_name".to_sym) source_email = clean(commit.send "#{options[:source]}_email".to_sym) diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 6ffa1a7121d..9d718e13b85 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -167,4 +167,23 @@ module DiffHelper content_tag(:span, commit_id, class: 'monospace'), ].join(' ').html_safe end + + def commit_for_diff(diff) + if diff.deleted_file + @merge_request ? @merge_request.commits.last : @commit.parent_id + else + @commit + end + end + + def diff_file_html_data(project, diff_commit, diff_file) + { + blob_diff_path: namespace_project_blob_diff_path(project.namespace, project, + tree_join(diff_commit.id, diff_file.file_path)) + } + end + + def editable_diff?(diff) + !diff.deleted_file && @merge_request && @merge_request.source_project + end end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 76602614bcd..6f69c2a9f32 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -46,6 +46,14 @@ module EventsHelper } end + def event_preposition(event) + if event.push? || event.commented? || event.target + "at" + elsif event.milestone? + "in" + end + end + def event_feed_title(event) words = [] words << event.author_name @@ -62,6 +70,9 @@ module EventsHelper words << "##{truncate event.note_target_iid}" end words << "at" + elsif event.milestone? + words << "##{event.target_iid}" if event.target_iid + words << "in" elsif event.target words << "##{event.target_iid}:" words << event.target.title if event.target.respond_to?(:title) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 1ebfd92f119..153a44870f6 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -45,7 +45,7 @@ module GitlabMarkdownHelper end def markdown(text, context = {}) - context.merge!( + context.reverse_merge!( current_user: current_user, path: @path, project: @project, @@ -59,7 +59,7 @@ module GitlabMarkdownHelper # TODO (rspeicher): Remove all usages of this helper and just call `markdown` # with a custom pipeline depending on the content being rendered def gfm(text, options = {}) - options.merge!( + options.reverse_merge!( current_user: current_user, path: @path, project: @project, @@ -165,7 +165,7 @@ module GitlabMarkdownHelper # and return true. Otherwise return false. def truncate_if_block(node, truncated) if node.element? && node.description.block? && !truncated - node.content = "#{node.content}..." if node.next_sibling + node.inner_html = "#{node.inner_html}..." if node.next_sibling true else truncated diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index d0fae255a04..4d9da6ff837 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -17,6 +17,14 @@ module GitlabRoutingHelper namespace_project_path(project.namespace, project, *args) end + def project_files_path(project, *args) + namespace_project_tree_path(project.namespace, project, @ref || project.repository.root_ref) + end + + def project_commits_path(project, *args) + namespace_project_commits_path(project.namespace, project, @ref || project.repository.root_ref) + end + def activity_project_path(project, *args) activity_namespace_project_path(project.namespace, project, *args) end @@ -25,6 +33,14 @@ module GitlabRoutingHelper edit_namespace_project_path(project.namespace, project, *args) end + def runners_path(project, *args) + namespace_project_runners_path(project.namespace, project, *args) + end + + def runner_path(runner, *args) + namespace_project_runner_path(@project.namespace, @project, runner, *args) + end + def issue_path(entity, *args) namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args) end diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb index e1dda20de85..1e372d5631d 100644 --- a/app/helpers/graph_helper.rb +++ b/app/helpers/graph_helper.rb @@ -1,7 +1,10 @@ module GraphHelper def get_refs(repo, commit) refs = "" - refs << commit.ref_names(repo).join(' ') + # Commit::ref_names already strips the refs/XXX from important refs (e.g. refs/heads/XXX) + # so anything leftover is internally used by GitLab + commit_refs = commit.ref_names(repo).reject{ |name| name.starts_with?('refs/') } + refs << commit_refs.join(' ') # append note count refs << "[#{@graph.notes[commit.id]}]" if @graph.notes[commit.id] > 0 diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 82eebf4245b..1d36969cd62 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -27,16 +27,16 @@ module GroupsHelper if group && group.avatar.present? group.avatar.url else - image_path('no_group_avatar.png') + 'no_group_avatar.png' end end - def group_title(group, name, url) + def group_title(group, name = nil, url = nil) + full_title = link_to(simple_sanitize(group.name), group_path(group)) + full_title += ' · '.html_safe + link_to(simple_sanitize(name), url) if name + content_tag :span do - link_to( - simple_sanitize(group.name), group_path(group) - ) + ' · '.html_safe + - link_to(simple_sanitize(name), url) + full_title end end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 2f8e64c375f..cf11f8e5320 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -12,4 +12,49 @@ module NotificationsHelper icon('circle-o', class: 'ns-default') end end + + def notification_list_item(notification_level, user_membership) + case notification_level + when Notification::N_DISABLED + content_tag(:li, class: active_level_for(user_membership, Notification::N_DISABLED)) do + link_to '#', class: 'update-notification', data: { notification_level: Notification::N_DISABLED } do + icon('microphone-slash fw', text: 'Disabled') + end + end + when Notification::N_PARTICIPATING + content_tag(:li, class: active_level_for(user_membership, Notification::N_PARTICIPATING)) do + link_to '#', class: 'update-notification', data: { notification_level: Notification::N_PARTICIPATING } do + icon('volume-up fw', text: 'Participate') + end + end + when Notification::N_WATCH + content_tag(:li, class: active_level_for(user_membership, Notification::N_WATCH)) do + link_to '#', class: 'update-notification', data: { notification_level: Notification::N_WATCH } do + icon('eye fw', text: 'Watch') + end + end + when Notification::N_MENTION + content_tag(:li, class: active_level_for(user_membership, Notification::N_MENTION)) do + link_to '#', class: 'update-notification', data: { notification_level: Notification::N_MENTION } do + icon('at fw', text: 'On mention') + end + end + when Notification::N_GLOBAL + content_tag(:li, class: active_level_for(user_membership, Notification::N_GLOBAL)) do + link_to '#', class: 'update-notification', data: { notification_level: Notification::N_GLOBAL } do + icon('globe fw', text: 'Global') + end + end + else + # do nothing + end + end + + def notification_label(user_membership) + Notification.new(user_membership).to_s + end + + def active_level_for(user_membership, level) + 'active' if user_membership.notification_level == level + end end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 8473d6d75d0..df37be51ce9 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -31,4 +31,26 @@ module PageLayoutHelper @fluid_layout end end + + def blank_container(enabled = false) + if @blank_container.nil? + @blank_container = enabled + else + @blank_container + end + end + + def container_class + css_class = "container-fluid" + + unless fluid_layout + css_class += " container-limited" + end + + if blank_container + css_class += " container-blank" + end + + css_class + end end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 7f1b6a69926..1b1f4162df4 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -3,7 +3,9 @@ module PreferencesHelper # Maps `dashboard` values to more user-friendly option text DASHBOARD_CHOICES = { projects: 'Your Projects (default)', - stars: 'Starred Projects' + stars: 'Starred Projects', + project_activity: "Your Projects' Activity", + starred_project_activity: "Starred Projects' Activity" }.with_indifferent_access.freeze # Returns an Array usable by a select field for more user-friendly option text diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index ab9b068de05..7b4747ce3d7 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -43,24 +43,22 @@ module ProjectsHelper end end - def project_title(project) - if project.group - content_tag :span do - link_to( - simple_sanitize(project.group.name), group_path(project.group) - ) + ' / ' + - link_to(simple_sanitize(project.name), - project_path(project)) - end - else - owner = project.namespace.owner - content_tag :span do - link_to( - simple_sanitize(owner.name), user_path(owner) - ) + ' / ' + - link_to(simple_sanitize(project.name), - project_path(project)) + def project_title(project, name = nil, url = nil) + namespace_link = + if project.group + link_to(simple_sanitize(project.group.name), group_path(project.group)) + else + owner = project.namespace.owner + link_to(simple_sanitize(owner.name), user_path(owner)) end + + project_link = link_to(simple_sanitize(project.name), project_path(project)) + + full_title = namespace_link + ' / ' + project_link + full_title += ' · '.html_safe + link_to(simple_sanitize(name), url) if name + + content_tag :span do + full_title end end @@ -158,8 +156,8 @@ module ProjectsHelper end end - def repository_size(project = nil) - "#{(project || @project).repository_size} MB" + def repository_size(project = @project) + "#{project.repository_size} MB" rescue # In order to prevent 500 error # when application cannot allocate memory @@ -315,6 +313,10 @@ module ProjectsHelper end end + def current_ref + @ref || @repository.try(:root_ref) + end + private def filename_path(project, filename) diff --git a/app/helpers/runners_helper.rb b/app/helpers/runners_helper.rb new file mode 100644 index 00000000000..5d7d06c8490 --- /dev/null +++ b/app/helpers/runners_helper.rb @@ -0,0 +1,20 @@ +module RunnersHelper + def runner_status_icon(runner) + unless runner.contacted_at + return content_tag :i, nil, + class: "fa fa-warning-sign", + title: "New runner. Has not connected yet" + end + + status = + if runner.active? + runner.contacted_at > 3.hour.ago ? :online : :offline + else + :paused + end + + content_tag :i, nil, + class: "fa fa-circle runner-status-#{status}", + title: "Runner is #{status}, last contact was #{time_ago_in_words(runner.contacted_at)} ago" + end +end diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb new file mode 100644 index 00000000000..8142f733e76 --- /dev/null +++ b/app/helpers/time_helper.rb @@ -0,0 +1,27 @@ +module TimeHelper + def duration_in_words(finished_at, started_at) + if finished_at && started_at + interval_in_seconds = finished_at.to_i - started_at.to_i + elsif started_at + interval_in_seconds = Time.now.to_i - started_at.to_i + end + + time_interval_in_words(interval_in_seconds) + end + + def time_interval_in_words(interval_in_seconds) + minutes = interval_in_seconds / 60 + seconds = interval_in_seconds - minutes * 60 + + if minutes >= 1 + "#{pluralize(minutes, "minute")} #{pluralize(seconds, "second")}" + else + "#{pluralize(seconds, "second")}" + end + end + + + def date_from_to(from, to) + "#{from.to_s(:short)} - #{to.to_s(:short)}" + end +end diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb new file mode 100644 index 00000000000..2a3a7e80fca --- /dev/null +++ b/app/helpers/triggers_helper.rb @@ -0,0 +1,5 @@ +module TriggersHelper + def ci_build_trigger_url(project_id, ref_name) + "#{Settings.gitlab_ci.url}/ci/api/v1/projects/#{project_id}/refs/#{ref_name}/trigger" + end +end diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb index f64d730b448..a674564c4ec 100644 --- a/app/helpers/version_check_helper.rb +++ b/app/helpers/version_check_helper.rb @@ -1,6 +1,6 @@ module VersionCheckHelper def version_status_badge - if Rails.env.production? + if Rails.env.production? && current_application_settings.version_check_enabled image_tag VersionCheck.new.url end end diff --git a/app/mailers/ci/emails/builds.rb b/app/mailers/ci/emails/builds.rb new file mode 100644 index 00000000000..6fb4fba85e5 --- /dev/null +++ b/app/mailers/ci/emails/builds.rb @@ -0,0 +1,17 @@ +module Ci + module Emails + module Builds + def build_fail_email(build_id, to) + @build = Ci::Build.find(build_id) + @project = @build.project + mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha)) + end + + def build_success_email(build_id, to) + @build = Ci::Build.find(build_id) + @project = @build.project + mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha)) + end + end + end +end diff --git a/app/mailers/ci/notify.rb b/app/mailers/ci/notify.rb new file mode 100644 index 00000000000..404842cf213 --- /dev/null +++ b/app/mailers/ci/notify.rb @@ -0,0 +1,46 @@ +module Ci + class Notify < ActionMailer::Base + include Ci::Emails::Builds + + add_template_helper Ci::GitlabHelper + + default_url_options[:host] = Gitlab.config.gitlab.host + default_url_options[:protocol] = Gitlab.config.gitlab.protocol + default_url_options[:port] = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port? + default_url_options[:script_name] = Gitlab.config.gitlab.relative_url_root + + default from: Gitlab.config.gitlab.email_from + + # Just send email with 3 seconds delay + def self.delay + delay_for(2.seconds) + end + + private + + # Formats arguments into a String suitable for use as an email subject + # + # extra - Extra Strings to be inserted into the subject + # + # Examples + # + # >> subject('Lorem ipsum') + # => "GitLab-CI | Lorem ipsum" + # + # # Automatically inserts Project name when @project is set + # >> @project = Project.last + # => #<Project id: 1, name: "Ruby on Rails", path: "ruby_on_rails", ...> + # >> subject('Lorem ipsum') + # => "GitLab-CI | Ruby on Rails | Lorem ipsum " + # + # # Accepts multiple arguments + # >> subject('Lorem ipsum', 'Dolor sit amet') + # => "GitLab-CI | Lorem ipsum | Dolor sit amet" + def subject(*extra) + subject = "GitLab-CI" + subject << (@project ? " | #{@project.name}" : "") + subject << " | " + extra.join(' | ') if extra.present? + subject + end + end +end diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index 63d4aca61af..87ba94a583d 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -12,7 +12,7 @@ module Emails to: recipient(recipient_id), subject: subject("#{@commit.title} (#{@commit.short_id})")) - SentNotification.record(@commit, recipient_id, reply_key) + SentNotification.record_note(@note, recipient_id, reply_key) end def note_issue_email(recipient_id, note_id) @@ -27,7 +27,7 @@ module Emails to: recipient(recipient_id), subject: subject("#{@issue.title} (##{@issue.iid})")) - SentNotification.record(@issue, recipient_id, reply_key) + SentNotification.record_note(@note, recipient_id, reply_key) end def note_merge_request_email(recipient_id, note_id) @@ -43,7 +43,7 @@ module Emails to: recipient(recipient_id), subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) - SentNotification.record(@merge_request, recipient_id, reply_key) + SentNotification.record_note(@note, recipient_id, reply_key) end end end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 5717c89e61d..db2f9654e14 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -100,7 +100,7 @@ class Notify < BaseMailer def mail_thread(model, headers = {}) if @project - headers['X-GitLab-Project'] = @project.name + headers['X-GitLab-Project'] = @project.name headers['X-GitLab-Project-Id'] = @project.id headers['X-GitLab-Project-Path'] = @project.path_with_namespace end @@ -110,7 +110,7 @@ class Notify < BaseMailer if reply_key headers['X-GitLab-Reply-Key'] = reply_key - address = Mail::Address.new(Gitlab::ReplyByEmail.reply_address(reply_key)) + address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key)) address.display_name = @project.name_with_namespace headers['Reply-To'] = address @@ -150,6 +150,6 @@ class Notify < BaseMailer end def reply_key - @reply_key ||= Gitlab::ReplyByEmail.reply_key + @reply_key ||= SentNotification.reply_key end end diff --git a/app/models/ability.rb b/app/models/ability.rb index f8e5afa9b01..a020b24a550 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -149,6 +149,7 @@ class Ability :admin_merge_request, :create_merge_request, :create_wiki, + :manage_builds, :push_code ] end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 8f27e35d723..784f5c96a0a 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -83,7 +83,8 @@ class ApplicationSetting < ActiveRecord::Base default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], restricted_signup_domains: Settings.gitlab['restricted_signup_domains'], - import_sources: ['github','bitbucket','gitlab','gitorious','google_code','git'] + import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'], + ci_enabled: Settings.gitlab_ci['enabled'] ) end diff --git a/app/models/ci/application_setting.rb b/app/models/ci/application_setting.rb new file mode 100644 index 00000000000..0cf496f7d81 --- /dev/null +++ b/app/models/ci/application_setting.rb @@ -0,0 +1,27 @@ +# == Schema Information +# +# Table name: application_settings +# +# id :integer not null, primary key +# all_broken_builds :boolean +# add_pusher :boolean +# created_at :datetime +# updated_at :datetime +# + +module Ci + class ApplicationSetting < ActiveRecord::Base + extend Ci::Model + + def self.current + Ci::ApplicationSetting.last + end + + def self.create_from_defaults + create( + all_broken_builds: Settings.gitlab_ci['all_broken_builds'], + add_pusher: Settings.gitlab_ci['add_pusher'], + ) + end + end +end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb new file mode 100644 index 00000000000..cda4fdd4982 --- /dev/null +++ b/app/models/ci/build.rb @@ -0,0 +1,283 @@ +# == 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) +# options :text +# allow_failure :boolean default(FALSE), not null +# stage :string(255) +# deploy :boolean default(FALSE) +# trigger_request_id :integer +# + +module Ci + class Build < ActiveRecord::Base + extend Ci::Model + + LAZY_ATTRIBUTES = ['trace'] + + belongs_to :commit, class_name: 'Ci::Commit' + belongs_to :runner, class_name: 'Ci::Runner' + belongs_to :trigger_request, class_name: 'Ci::TriggerRequest' + + serialize :options + + validates :commit, presence: true + validates :status, presence: true + validates :coverage, numericality: true, allow_blank: true + + scope :running, ->() { where(status: "running") } + scope :pending, ->() { where(status: "pending") } + scope :success, ->() { where(status: "success") } + scope :failed, ->() { where(status: "failed") } + scope :unstarted, ->() { where(runner_id: nil) } + scope :running_or_pending, ->() { where(status:[:running, :pending]) } + + acts_as_taggable + + # To prevent db load megabytes of data from trace + default_scope -> { select(Ci::Build.columns_without_lazy) } + + class << self + def columns_without_lazy + (column_names - LAZY_ATTRIBUTES).map do |column_name| + "#{table_name}.#{column_name}" + end + end + + def last_month + where('created_at > ?', Date.today - 1.month) + end + + def first_pending + pending.unstarted.order('created_at ASC').first + end + + def create_from(build) + new_build = build.dup + new_build.status = :pending + new_build.runner_id = nil + new_build.save + end + + def retry(build) + new_build = Ci::Build.new(status: :pending) + new_build.options = build.options + new_build.commands = build.commands + new_build.tag_list = build.tag_list + new_build.commit_id = build.commit_id + new_build.name = build.name + new_build.allow_failure = build.allow_failure + new_build.stage = build.stage + new_build.trigger_request = build.trigger_request + new_build.save + new_build + end + end + + state_machine :status, initial: :pending do + event :run do + transition pending: :running + end + + event :drop do + transition running: :failed + end + + event :success do + transition running: :success + end + + event :cancel do + transition [:pending, :running] => :canceled + end + + after_transition pending: :running do |build, transition| + build.update_attributes started_at: Time.now + end + + after_transition any => [:success, :failed, :canceled] do |build, transition| + build.update_attributes finished_at: Time.now + project = build.project + + if project.web_hooks? + Ci::WebHookService.new.build_end(build) + end + + if build.commit.success? + build.commit.create_next_builds(build.trigger_request) + end + + project.execute_services(build) + + if project.coverage_enabled? + build.update_coverage + end + end + + state :pending, value: 'pending' + state :running, value: 'running' + state :failed, value: 'failed' + state :success, value: 'success' + state :canceled, value: 'canceled' + end + + delegate :sha, :short_sha, :before_sha, :ref, :project, + to: :commit, prefix: false + + def trace_html + html = Ci::Ansi2html::convert(trace) if trace.present? + html ||= '' + end + + def trace + if project && read_attribute(:trace).present? + read_attribute(:trace).gsub(project.token, 'xxxxxx') + end + end + + def started? + !pending? && !canceled? && started_at + end + + def active? + running? || pending? + end + + def complete? + canceled? || success? || failed? + end + + def ignored? + failed? && allow_failure? + end + + def timeout + project.timeout + end + + def variables + yaml_variables + project_variables + trigger_variables + end + + def duration + if started_at && finished_at + finished_at - started_at + elsif started_at + Time.now - started_at + end + end + + def project + commit.project + end + + def project_id + commit.project.id + end + + def project_name + project.name + end + + def repo_url + project.repo_url_with_auth + end + + def allow_git_fetch + project.allow_git_fetch + end + + def update_coverage + coverage = extract_coverage(trace, project.coverage_regex) + + if coverage.is_a? Numeric + update_attributes(coverage: coverage) + end + end + + def extract_coverage(text, regex) + begin + matches = text.gsub(Regexp.new(regex)).to_a.last + coverage = matches.gsub(/\d+(\.\d+)?/).first + + if coverage.present? + coverage.to_f + end + rescue => ex + # if bad regex or something goes wrong we dont want to interrupt transition + # so we just silentrly ignore error for now + end + end + + def trace + if File.exist?(path_to_trace) + File.read(path_to_trace) + else + # backward compatibility + read_attribute :trace + end + end + + def trace=(trace) + unless Dir.exists? dir_to_trace + FileUtils.mkdir_p dir_to_trace + end + + File.write(path_to_trace, trace) + end + + def dir_to_trace + File.join( + Settings.gitlab_ci.builds_path, + created_at.utc.strftime("%Y_%m"), + project.id.to_s + ) + end + + def path_to_trace + "#{dir_to_trace}/#{id}.log" + end + + private + + def yaml_variables + if commit.config_processor + commit.config_processor.variables.map do |key, value| + { key: key, value: value, public: true } + end + else + [] + end + end + + def project_variables + project.variables.map do |variable| + { key: variable.key, value: variable.value, public: false } + end + end + + def trigger_variables + if trigger_request && trigger_request.variables + trigger_request.variables.map do |key, value| + { key: key, value: value, public: false } + end + else + [] + end + end + end +end diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb new file mode 100644 index 00000000000..6d048779cde --- /dev/null +++ b/app/models/ci/commit.rb @@ -0,0 +1,274 @@ +# == 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 +# + +module Ci + class Commit < ActiveRecord::Base + extend Ci::Model + + belongs_to :gl_project, class_name: '::Project', foreign_key: :gl_project_id + has_many :builds, dependent: :destroy, class_name: 'Ci::Build' + has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' + + serialize :push_data + + validates_presence_of :ref, :sha, :before_sha, :push_data + validate :valid_commit_sha + + def self.truncate_sha(sha) + sha[0...8] + end + + def to_param + sha + end + + def project + @project ||= gl_project.ensure_gitlab_ci_project + end + + def project_id + project.id + end + + def last_build + builds.order(:id).last + end + + def retry + builds_without_retry.each do |build| + Ci::Build.retry(build) + end + end + + def valid_commit_sha + if self.sha == Ci::Git::BLANK_SHA + self.errors.add(:sha, " cant be 00000000 (branch removal)") + end + end + + def new_branch? + before_sha == Ci::Git::BLANK_SHA + end + + def compare? + !new_branch? + end + + def git_author_name + commit_data[:author][:name] if commit_data && commit_data[:author] + end + + def git_author_email + commit_data[:author][:email] if commit_data && commit_data[:author] + end + + def git_commit_message + commit_data[:message] if commit_data && commit_data[:message] + end + + def short_before_sha + Ci::Commit.truncate_sha(before_sha) + end + + def short_sha + Ci::Commit.truncate_sha(sha) + end + + def commit_data + push_data[:commits].find do |commit| + commit[:id] == sha + end + rescue + nil + end + + def project_recipients + recipients = project.email_recipients.split(' ') + + if project.email_add_pusher? && push_data[:user_email].present? + recipients << push_data[:user_email] + end + + recipients.uniq + end + + def stage + return unless config_processor + stages = builds_without_retry.select(&:active?).map(&:stage) + config_processor.stages.find { |stage| stages.include? stage } + end + + def create_builds_for_stage(stage, trigger_request) + return if skip_ci? && trigger_request.blank? + return unless config_processor + + builds_attrs = config_processor.builds_for_stage_and_ref(stage, ref, tag) + builds_attrs.map do |build_attrs| + builds.create!({ + name: build_attrs[:name], + commands: build_attrs[:script], + tag_list: build_attrs[:tags], + options: build_attrs[:options], + allow_failure: build_attrs[:allow_failure], + stage: build_attrs[:stage], + trigger_request: trigger_request, + }) + end + end + + def create_next_builds(trigger_request) + return if skip_ci? && trigger_request.blank? + return unless config_processor + + stages = builds.where(trigger_request: trigger_request).group_by(&:stage) + + config_processor.stages.any? do |stage| + !stages.include?(stage) && create_builds_for_stage(stage, trigger_request).present? + end + end + + def create_builds(trigger_request = nil) + return if skip_ci? && trigger_request.blank? + return unless config_processor + + config_processor.stages.any? do |stage| + create_builds_for_stage(stage, trigger_request).present? + end + end + + def builds_without_retry + @builds_without_retry ||= + begin + grouped_builds = builds.group_by(&:name) + grouped_builds.map do |name, builds| + builds.sort_by(&:id).last + end + end + end + + def builds_without_retry_sorted + return builds_without_retry unless config_processor + + stages = config_processor.stages + builds_without_retry.sort_by do |build| + [stages.index(build.stage) || -1, build.name || ""] + end + end + + def retried_builds + @retried_builds ||= (builds.order(id: :desc) - builds_without_retry) + end + + def status + if skip_ci? + return 'skipped' + elsif yaml_errors.present? + return 'failed' + elsif builds.none? + return 'skipped' + elsif success? + 'success' + elsif pending? + 'pending' + elsif running? + 'running' + elsif canceled? + 'canceled' + else + 'failed' + end + end + + def pending? + builds_without_retry.all? do |build| + build.pending? + end + end + + def running? + builds_without_retry.any? do |build| + build.running? || build.pending? + end + end + + def success? + builds_without_retry.all? do |build| + build.success? || build.ignored? + end + end + + def failed? + status == 'failed' + end + + def canceled? + builds_without_retry.all? do |build| + build.canceled? + end + end + + def duration + @duration ||= builds_without_retry.select(&:duration).sum(&:duration).to_i + end + + def finished_at + @finished_at ||= builds.order('finished_at DESC').first.try(:finished_at) + end + + def coverage + if project.coverage_enabled? + coverage_array = builds_without_retry.map(&:coverage).compact + if coverage_array.size >= 1 + '%.2f' % (coverage_array.reduce(:+) / coverage_array.size) + end + end + end + + def matrix? + builds_without_retry.size > 1 + end + + def config_processor + @config_processor ||= Ci::GitlabCiYamlProcessor.new(push_data[:ci_yaml_file]) + rescue Ci::GitlabCiYamlProcessor::ValidationError => e + save_yaml_error(e.message) + nil + rescue Exception => e + logger.error e.message + "\n" + e.backtrace.join("\n") + save_yaml_error("Undefined yaml error") + nil + end + + def skip_ci? + return false if builds.any? + commits = push_data[:commits] + commits.present? && commits.last[:message] =~ /(\[ci skip\])/ + end + + def update_committed! + update!(committed_at: DateTime.now) + end + + private + + def save_yaml_error(error) + return if self.yaml_errors? + self.yaml_errors = error + save + end + end +end diff --git a/app/models/ci/event.rb b/app/models/ci/event.rb new file mode 100644 index 00000000000..cac3a7a49c1 --- /dev/null +++ b/app/models/ci/event.rb @@ -0,0 +1,27 @@ +# == 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 +# + +module Ci + class Event < ActiveRecord::Base + extend Ci::Model + + belongs_to :project, class_name: 'Ci::Project' + + validates :description, + presence: true, + length: { in: 5..200 } + + scope :admin, ->(){ where(is_admin: true) } + scope :project_wide, ->(){ where(is_admin: false) } + end +end diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb new file mode 100644 index 00000000000..f0a8fc703b5 --- /dev/null +++ b/app/models/ci/project.rb @@ -0,0 +1,216 @@ +# == 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 +# + +module Ci + class Project < ActiveRecord::Base + extend Ci::Model + + include Ci::ProjectStatus + + belongs_to :gl_project, class_name: '::Project', foreign_key: :gitlab_id + + has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' + has_many :runners, through: :runner_projects, class_name: 'Ci::Runner' + has_many :web_hooks, dependent: :destroy, class_name: 'Ci::WebHook' + has_many :events, dependent: :destroy, class_name: 'Ci::Event' + has_many :variables, dependent: :destroy, class_name: 'Ci::Variable' + has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger' + + # Project services + has_many :services, dependent: :destroy, class_name: 'Ci::Service' + has_one :hip_chat_service, dependent: :destroy, class_name: 'Ci::HipChatService' + has_one :slack_service, dependent: :destroy, class_name: 'Ci::SlackService' + has_one :mail_service, dependent: :destroy, class_name: 'Ci::MailService' + + accepts_nested_attributes_for :variables, allow_destroy: true + + delegate :name_with_namespace, :path_with_namespace, :web_url, :http_url_to_repo, :ssh_url_to_repo, to: :gl_project + + # + # Validations + # + validates_presence_of :timeout, :token, :default_ref, :gitlab_id + + validates_uniqueness_of :gitlab_id + + validates :polling_interval, + presence: true, + if: ->(project) { project.always_build.present? } + + before_validation :set_default_values + + class << self + include Ci::CurrentSettings + + def base_build_script + <<-eos + git submodule update --init + ls -la + eos + end + + def parse(project) + params = { + gitlab_id: project.id, + default_ref: project.default_branch || 'master', + email_add_pusher: current_application_settings.add_pusher, + email_only_broken_builds: current_application_settings.all_broken_builds, + } + + project = Ci::Project.new(params) + project.build_missing_services + project + end + + def already_added?(project) + where(gitlab_id: project.id).any? + end + + def unassigned(runner) + joins("LEFT JOIN #{Ci::RunnerProject.table_name} ON #{Ci::RunnerProject.table_name}.project_id = #{Ci::Project.table_name}.id " \ + "AND #{Ci::RunnerProject.table_name}.runner_id = #{runner.id}"). + where("#{Ci::RunnerProject.table_name}.project_id" => nil) + end + + def ordered_by_last_commit_date + last_commit_subquery = "(SELECT gl_project_id, MAX(committed_at) committed_at FROM #{Ci::Commit.table_name} GROUP BY gl_project_id)" + joins("LEFT JOIN #{last_commit_subquery} AS last_commit ON #{Ci::Project.table_name}.gitlab_id = last_commit.gl_project_id"). + order("CASE WHEN last_commit.committed_at IS NULL THEN 1 ELSE 0 END, last_commit.committed_at DESC") + end + end + + def name + name_with_namespace + end + + def path + path_with_namespace + end + + def gitlab_url + web_url + end + + def any_runners? + if runners.active.any? + return true + end + + shared_runners_enabled && Ci::Runner.shared.active.any? + end + + def set_default_values + self.token = SecureRandom.hex(15) if self.token.blank? + self.default_ref ||= 'master' + end + + def tracked_refs + @tracked_refs ||= default_ref.split(",").map { |ref| ref.strip } + end + + def valid_token? token + self.token && self.token == token + end + + def no_running_builds? + # Get running builds not later than 3 days ago to ignore hangs + builds.running.where("updated_at > ?", 3.days.ago).empty? + end + + def email_notification? + email_add_pusher || email_recipients.present? + end + + def web_hooks? + web_hooks.any? + end + + def services? + services.any? + end + + def timeout_in_minutes + timeout / 60 + end + + def timeout_in_minutes=(value) + self.timeout = value.to_i * 60 + end + + def coverage_enabled? + coverage_regex.present? + end + + # Build a clone-able repo url + # using http and basic auth + def repo_url_with_auth + auth = "gitlab-ci-token:#{token}@" + url = http_url_to_repo + ".git" + url.sub(/^https?:\/\//) do |prefix| + prefix + auth + end + end + + def available_services_names + %w(slack mail hip_chat) + end + + def build_missing_services + available_services_names.each do |service_name| + service = services.find { |service| service.to_param == service_name } + + # If service is available but missing in db + # we should create an instance. Ex `create_gitlab_ci_service` + service = self.send :"create_#{service_name}_service" if service.nil? + end + end + + def execute_services(data) + services.each do |service| + + # Call service hook only if it is active + begin + service.execute(data) if service.active && service.can_execute?(data) + rescue => e + logger.error(e) + end + end + end + + def setup_finished? + commits.any? + end + + def commits + gl_project.ci_commits + end + + def builds + gl_project.ci_builds + end + end +end diff --git a/app/models/ci/project_status.rb b/app/models/ci/project_status.rb new file mode 100644 index 00000000000..6d5cafe81a2 --- /dev/null +++ b/app/models/ci/project_status.rb @@ -0,0 +1,47 @@ +module Ci + module ProjectStatus + def status + last_commit.status if last_commit + end + + def broken? + last_commit.failed? if last_commit + end + + def success? + last_commit.success? if last_commit + end + + def broken_or_success? + broken? || success? + end + + def last_commit + @last_commit ||= commits.last if commits.any? + end + + def last_commit_date + last_commit.try(:created_at) + end + + def human_status + status + end + + # only check for toggling build status within same ref. + def last_commit_changed_status? + ref = last_commit.ref + last_commits = commits.where(ref: ref).last(2) + + if last_commits.size < 2 + false + else + last_commits[0].status != last_commits[1].status + end + end + + def last_commit_for_ref(ref) + commits.where(ref: ref).last + end + end +end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb new file mode 100644 index 00000000000..6838ccfaaab --- /dev/null +++ b/app/models/ci/runner.rb @@ -0,0 +1,84 @@ +# == 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) +# + +module Ci + class Runner < ActiveRecord::Base + extend Ci::Model + + has_many :builds, class_name: 'Ci::Build' + has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' + has_many :projects, through: :runner_projects, class_name: 'Ci::Project' + + has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build' + + before_validation :set_default_values + + scope :specific, ->() { where(is_shared: false) } + scope :shared, ->() { where(is_shared: true) } + scope :active, ->() { where(active: true) } + scope :paused, ->() { where(active: false) } + + acts_as_taggable + + def self.search(query) + where('LOWER(ci_runners.token) LIKE :query OR LOWER(ci_runners.description) like :query', + query: "%#{query.try(:downcase)}%") + end + + def gl_projects_ids + projects.select(:gitlab_id) + end + + def set_default_values + self.token = SecureRandom.hex(15) if self.token.blank? + end + + def assign_to(project, current_user = nil) + self.is_shared = false if shared? + self.save + project.runner_projects.create!(runner_id: self.id) + end + + def display_name + return token unless !description.blank? + + description + end + + def shared? + is_shared + end + + def belongs_to_one_project? + runner_projects.count == 1 + end + + def specific? + !shared? + end + + def only_for?(project) + projects == [project] + end + + def short_sha + token[0...10] + end + end +end diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb new file mode 100644 index 00000000000..44453ee4b41 --- /dev/null +++ b/app/models/ci/runner_project.rb @@ -0,0 +1,21 @@ +# == 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 +# + +module Ci + class RunnerProject < ActiveRecord::Base + extend Ci::Model + + belongs_to :runner, class_name: 'Ci::Runner' + belongs_to :project, class_name: 'Ci::Project' + + validates_uniqueness_of :runner_id, scope: :project_id + end +end diff --git a/app/models/ci/service.rb b/app/models/ci/service.rb new file mode 100644 index 00000000000..ed5e3f940b6 --- /dev/null +++ b/app/models/ci/service.rb @@ -0,0 +1,105 @@ +# == 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 +# + +# To add new service you should build a class inherited from Service +# and implement a set of methods +module Ci + class Service < ActiveRecord::Base + extend Ci::Model + + serialize :properties, JSON + + default_value_for :active, false + + after_initialize :initialize_properties + + belongs_to :project, class_name: 'Ci::Project' + + validates :project_id, presence: true + + def activated? + active + end + + def category + :common + end + + def initialize_properties + self.properties = {} if properties.nil? + end + + def title + # implement inside child + end + + def description + # implement inside child + end + + def help + # implement inside child + end + + def to_param + # implement inside child + end + + def fields + # implement inside child + [] + end + + def can_test? + project.builds.any? + end + + def can_execute?(build) + true + end + + def execute(build) + # implement inside child + end + + # Provide convenient accessor methods + # for each serialized property. + def self.prop_accessor(*args) + args.each do |arg| + class_eval %{ + def #{arg} + (properties || {})['#{arg}'] + end + + def #{arg}=(value) + self.properties ||= {} + self.properties['#{arg}'] = value + end + } + end + end + + def self.boolean_accessor(*args) + self.prop_accessor(*args) + + args.each do |arg| + class_eval %{ + def #{arg}? + ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg}) + end + } + end + end + end +end diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb new file mode 100644 index 00000000000..fe224b7dc70 --- /dev/null +++ b/app/models/ci/trigger.rb @@ -0,0 +1,39 @@ +# == Schema Information +# +# Table name: triggers +# +# id :integer not null, primary key +# token :string(255) +# project_id :integer not null +# deleted_at :datetime +# created_at :datetime +# updated_at :datetime +# + +module Ci + class Trigger < ActiveRecord::Base + extend Ci::Model + + acts_as_paranoid + + belongs_to :project, class_name: 'Ci::Project' + has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' + + validates_presence_of :token + validates_uniqueness_of :token + + before_validation :set_default_values + + def set_default_values + self.token = SecureRandom.hex(15) if self.token.blank? + end + + def last_trigger_request + trigger_requests.last + end + + def short_token + token[0...10] + end + end +end diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb new file mode 100644 index 00000000000..29cd9553394 --- /dev/null +++ b/app/models/ci/trigger_request.rb @@ -0,0 +1,23 @@ +# == Schema Information +# +# Table name: trigger_requests +# +# id :integer not null, primary key +# trigger_id :integer not null +# variables :text +# created_at :datetime +# updated_at :datetime +# commit_id :integer +# + +module Ci + class TriggerRequest < ActiveRecord::Base + extend Ci::Model + + belongs_to :trigger, class_name: 'Ci::Trigger' + belongs_to :commit, class_name: 'Ci::Commit' + has_many :builds, class_name: 'Ci::Build' + + serialize :variables + end +end diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb new file mode 100644 index 00000000000..7a542802fa6 --- /dev/null +++ b/app/models/ci/variable.rb @@ -0,0 +1,25 @@ +# == 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) +# + +module Ci + class Variable < ActiveRecord::Base + extend Ci::Model + + belongs_to :project, class_name: 'Ci::Project' + + validates_presence_of :key + validates_uniqueness_of :key, scope: :project_id + + attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base + end +end diff --git a/app/models/ci/web_hook.rb b/app/models/ci/web_hook.rb new file mode 100644 index 00000000000..8f03b0625da --- /dev/null +++ b/app/models/ci/web_hook.rb @@ -0,0 +1,44 @@ +# == 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 +# + +module Ci + class WebHook < ActiveRecord::Base + extend Ci::Model + + include HTTParty + + belongs_to :project, class_name: 'Ci::Project' + + # HTTParty timeout + default_timeout 10 + + validates :url, presence: true, + format: { with: URI::regexp(%w(http https)), message: "should be a valid url" } + + def execute(data) + parsed_url = URI.parse(url) + if parsed_url.userinfo.blank? + Ci::WebHook.post(url, body: data.to_json, headers: { "Content-Type" => "application/json" }, verify: false) + else + post_url = url.gsub("#{parsed_url.userinfo}@", "") + auth = { + username: URI.decode(parsed_url.user), + password: URI.decode(parsed_url.password), + } + Ci::WebHook.post(post_url, + body: data.to_json, + headers: { "Content-Type" => "application/json" }, + verify: false, + basic_auth: auth) + end + end + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 40642dc63ba..4db4ffb2e79 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -140,6 +140,12 @@ module Issuable { object_kind: self.class.name.underscore, user: user.hook_attrs, + repository: { + name: project.name, + url: project.url_to_repo, + description: project.description, + homepage: project.web_url + }, object_attributes: hook_attrs } end diff --git a/app/models/event.rb b/app/models/event.rb index 78f16c6304e..47600c57e35 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -27,6 +27,7 @@ class Event < ActiveRecord::Base MERGED = 7 JOINED = 8 # User joined project LEFT = 9 # User left project + DESTROYED = 10 delegate :name, :email, to: :author, prefix: true, allow_nil: true delegate :title, to: :issue, prefix: true, allow_nil: true @@ -48,6 +49,7 @@ class Event < ActiveRecord::Base scope :code_push, -> { where(action: PUSHED) } scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent } scope :with_associations, -> { includes(project: :namespace) } + scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) } class << self def reset_event_cache_for(target) @@ -71,7 +73,7 @@ class Event < ActiveRecord::Base elsif created_project? true else - (issue? || merge_request? || note? || milestone?) && target + ((issue? || merge_request? || note?) && target) || milestone? end end @@ -115,6 +117,10 @@ class Event < ActiveRecord::Base action == LEFT end + def destroyed? + action == DESTROYED + end + def commented? action == COMMENTED end @@ -124,7 +130,7 @@ class Event < ActiveRecord::Base end def created_project? - created? && !target + created? && !target && target_type.nil? end def created_target? @@ -180,6 +186,8 @@ class Event < ActiveRecord::Base 'joined' elsif left? 'left' + elsif destroyed? + 'destroyed' elsif commented? "commented on" elsif created_project? diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 9a8251bdad5..a078accbdbd 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -25,7 +25,7 @@ class WebHook < ActiveRecord::Base default_value_for :note_events, false default_value_for :merge_requests_events, false default_value_for :tag_push_events, false - default_value_for :enable_ssl_verification, false + default_value_for :enable_ssl_verification, true # HTTParty timeout default_timeout Gitlab.config.gitlab.webhook_timeout diff --git a/app/models/label.rb b/app/models/label.rb index 230631b5180..4a22bd53400 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -24,7 +24,7 @@ class Label < ActiveRecord::Base validates :color, format: { with: /\A#[0-9A-Fa-f]{6}\Z/ }, allow_blank: false - validates :project, presence: true + validates :project, presence: true, unless: Proc.new { |service| service.template? } # Don't allow '?', '&', and ',' for label titles validates :title, @@ -34,6 +34,8 @@ class Label < ActiveRecord::Base default_scope { order(title: :asc) } + scope :templates, -> { where(template: true) } + alias_attribute :name, :title def self.reference_prefix @@ -78,4 +80,8 @@ class Label < ActiveRecord::Base def open_issues_count issues.opened.count end + + def template? + template + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 93faa133875..eb468c6cd53 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -433,10 +433,22 @@ class MergeRequest < ActiveRecord::Base target_project.repository.fetch_ref( source_project.repository.path_to_repo, "refs/heads/#{source_branch}", - "refs/merge-requests/#{iid}/head" + ref_path ) end + def ref_path + "refs/merge-requests/#{iid}/head" + end + + def ref_is_fetched? + File.exists?(File.join(project.repository.path_to_repo, ref_path)) + end + + def ensure_ref_fetched + fetch_ref unless ref_is_fetched? + end + def in_locked_state begin lock_mr diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index e317c8eac4d..f75f999b0d0 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -123,12 +123,12 @@ class MergeRequestDiff < ActiveRecord::Base if new_diffs.any? if new_diffs.size > Commit::DIFF_HARD_LIMIT_FILES self.state = :overflow_diff_files_limit - new_diffs = new_diffs.first[Commit::DIFF_HARD_LIMIT_LINES] + new_diffs = new_diffs.first(Commit::DIFF_HARD_LIMIT_LINES) end if new_diffs.sum { |diff| diff.diff.lines.count } > Commit::DIFF_HARD_LIMIT_LINES self.state = :overflow_diff_lines_limit - new_diffs = new_diffs.first[Commit::DIFF_HARD_LIMIT_LINES] + new_diffs = new_diffs.first(Commit::DIFF_HARD_LIMIT_LINES) end end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index c6aff6f709f..d979a35084b 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -61,7 +61,7 @@ class Milestone < ActiveRecord::Base false end end - + def open_items_count self.issues.opened.count + self.merge_requests.opened.count end diff --git a/app/models/notification.rb b/app/models/notification.rb index 1395274173d..171b8df45c2 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -12,7 +12,7 @@ class Notification class << self def notification_levels - [N_DISABLED, N_PARTICIPATING, N_WATCH, N_MENTION] + [N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH] end def options_with_labels @@ -26,7 +26,7 @@ class Notification end def project_notification_levels - [N_DISABLED, N_PARTICIPATING, N_WATCH, N_GLOBAL, N_MENTION] + [N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH, N_GLOBAL] end end @@ -57,4 +57,21 @@ class Notification def level target.notification_level end + + def to_s + case level + when N_DISABLED + 'Disabled' + when N_PARTICIPATING + 'Participating' + when N_WATCH + 'Watching' + when N_MENTION + 'On mention' + when N_GLOBAL + 'Global' + else + # do nothing + end + end end diff --git a/app/models/project.rb b/app/models/project.rb index 8e33a4b2f0f..953b37e3f7a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -39,10 +39,13 @@ class Project < ActiveRecord::Base include Gitlab::VisibilityLevel include Referable include Sortable + include AfterCommitQueue extend Gitlab::ConfigHelper extend Enumerize + UNKNOWN_IMPORT_URL = 'http://unknown.git' + default_value_for :archived, false default_value_for :visibility_level, gitlab_config_features.visibility_level default_value_for :issues_enabled, gitlab_config_features.issues @@ -115,8 +118,11 @@ class Project < ActiveRecord::Base has_many :deploy_keys, through: :deploy_keys_projects has_many :users_star_projects, dependent: :destroy has_many :starrers, through: :users_star_projects, source: :user + has_many :ci_commits, ->() { order('CASE WHEN ci_commits.committed_at IS NULL THEN 0 ELSE 1 END', :committed_at, :id) }, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id + has_many :ci_builds, through: :ci_commits, source: :builds, dependent: :destroy, class_name: 'Ci::Build' has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" + has_one :gitlab_ci_project, dependent: :destroy, class_name: "Ci::Project", foreign_key: :gitlab_id delegate :name, to: :owner, allow_nil: true, prefix: true delegate :members, to: :team, prefix: true @@ -142,7 +148,7 @@ class Project < ActiveRecord::Base validates_uniqueness_of :path, scope: :namespace_id validates :import_url, format: { with: /\A#{URI.regexp(%w(ssh git http https))}\z/, message: 'should be a valid url' }, - if: :import? + if: :external_import? validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create validate :avatar_type, @@ -188,7 +194,7 @@ class Project < ActiveRecord::Base state :finished state :failed - after_transition any => :started, do: :add_import_job + after_transition any => :started, do: :schedule_add_import_job after_transition any => :finished, do: :clear_import_data end @@ -272,8 +278,18 @@ class Project < ActiveRecord::Base id && persisted? end + def schedule_add_import_job + run_after_commit(:add_import_job) + end + def add_import_job - RepositoryImportWorker.perform_in(2.seconds, id) + if forked? + unless RepositoryForkWorker.perform_async(id, forked_from_project.path_with_namespace, self.namespace.path) + import_fail + end + else + RepositoryImportWorker.perform_async(id) + end end def clear_import_data @@ -281,6 +297,10 @@ class Project < ActiveRecord::Base end def import? + external_import? || forked? + end + + def external_import? import_url.present? end @@ -317,7 +337,7 @@ class Project < ActiveRecord::Base end def web_url - Rails.application.routes.url_helpers.namespace_project_url(self.namespace, self) + Gitlab::Application.routes.url_helpers.namespace_project_url(self.namespace, self) end def web_url_without_protocol @@ -401,12 +421,21 @@ class Project < ActiveRecord::Base end end + def create_labels + Label.templates.each do |label| + label = label.dup + label.template = nil + label.project_id = self.id + label.save + end + end + def find_service(list, name) list.find { |service| service.to_param == name } end def gitlab_ci? - gitlab_ci_service && gitlab_ci_service.active + gitlab_ci_service && gitlab_ci_service.active && gitlab_ci_project.present? end def ci_services @@ -434,7 +463,7 @@ class Project < ActiveRecord::Base if avatar.present? [gitlab_config.url, avatar.url].join elsif avatar_in_git - Rails.application.routes.url_helpers.namespace_project_avatar_url(namespace, self) + Gitlab::Application.routes.url_helpers.namespace_project_avatar_url(namespace, self) end end @@ -691,14 +720,8 @@ class Project < ActiveRecord::Base end def create_repository - if forked? - if gitlab_shell.fork_repository(forked_from_project.path_with_namespace, self.namespace.path) - true - else - errors.add(:base, 'Failed to fork repository via gitlab-shell') - false - end - else + # Forked import is handled asynchronously + unless forked? if gitlab_shell.add_repository(path_with_namespace) true else @@ -719,4 +742,22 @@ class Project < ActiveRecord::Base errors.add(:base, 'Failed create wiki') false end + + def ci_commit(sha) + gitlab_ci_project.commits.find_by(sha: sha) if gitlab_ci? + end + + def ensure_gitlab_ci_project + gitlab_ci_project || create_gitlab_ci_project + end + + def enable_ci(user) + # Enable service + service = gitlab_ci_service || create_gitlab_ci_service + service.active = true + service.save + + # Create Ci::Project + Ci::CreateProjectService.new.execute(user, self) + end end diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index 9e5da6f45d2..40058b53df5 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -69,14 +69,6 @@ class BuildkiteService < CiService "#{project_url}/builds?commit=#{sha}" end - def builds_path - "#{project_url}/builds?branch=#{project.default_branch}" - end - - def status_img_path - "#{buildkite_endpoint('badge')}/#{status_token}.svg" - end - def title 'Buildkite' end diff --git a/app/models/project_services/ci/hip_chat_message.rb b/app/models/project_services/ci/hip_chat_message.rb new file mode 100644 index 00000000000..25c72033eac --- /dev/null +++ b/app/models/project_services/ci/hip_chat_message.rb @@ -0,0 +1,80 @@ +module Ci + class HipChatMessage + include Gitlab::Application.routes.url_helpers + + attr_reader :build + + def initialize(build) + @build = build + end + + def to_s + lines = Array.new + lines.push("<a href=\"#{ci_project_url(project)}\">#{project.name}</a> - ") + + if commit.matrix? + lines.push("<a href=\"#{ci_project_ref_commits_url(project, commit.ref, commit.sha)}\">Commit ##{commit.id}</a></br>") + else + first_build = commit.builds_without_retry.first + lines.push("<a href=\"#{ci_project_build_url(project, first_build)}\">Build '#{first_build.name}' ##{first_build.id}</a></br>") + end + + lines.push("#{commit.short_sha} #{commit.git_author_name} - #{commit.git_commit_message}</br>") + lines.push("#{humanized_status(commit_status)} in #{commit.duration} second(s).") + lines.join('') + end + + def status_color(build_or_commit=nil) + build_or_commit ||= commit_status + case build_or_commit + when :success + 'green' + when :failed, :canceled + 'red' + else # :pending, :running or unknown + 'yellow' + end + end + + def notify? + [:failed, :canceled].include?(commit_status) + end + + + private + + def commit + build.commit + end + + def project + commit.project + end + + def build_status + build.status.to_sym + end + + def commit_status + commit.status.to_sym + end + + def humanized_status(build_or_commit=nil) + build_or_commit ||= commit_status + case build_or_commit + when :pending + "Pending" + when :running + "Running" + when :failed + "Failed" + when :success + "Successful" + when :canceled + "Canceled" + else + "Unknown" + end + end + end +end diff --git a/app/models/project_services/ci/hip_chat_service.rb b/app/models/project_services/ci/hip_chat_service.rb new file mode 100644 index 00000000000..0e6e97394bc --- /dev/null +++ b/app/models/project_services/ci/hip_chat_service.rb @@ -0,0 +1,93 @@ +# == 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 +# + +module Ci + class HipChatService < Ci::Service + prop_accessor :hipchat_token, :hipchat_room, :hipchat_server + boolean_accessor :notify_only_broken_builds + validates :hipchat_token, presence: true, if: :activated? + validates :hipchat_room, presence: true, if: :activated? + default_value_for :notify_only_broken_builds, true + + def title + "HipChat" + end + + def description + "Private group chat, video chat, instant messaging for teams" + end + + def help + end + + def to_param + 'hip_chat' + end + + def fields + [ + { type: 'text', name: 'hipchat_token', label: 'Token', placeholder: '' }, + { type: 'text', name: 'hipchat_room', label: 'Room', placeholder: '' }, + { type: 'text', name: 'hipchat_server', label: 'Server', placeholder: 'https://hipchat.example.com', help: 'Leave blank for default' }, + { type: 'checkbox', name: 'notify_only_broken_builds', label: 'Notify only broken builds' } + ] + end + + def can_execute?(build) + return if build.allow_failure? + + commit = build.commit + return unless commit + return unless commit.builds_without_retry.include? build + + case commit.status.to_sym + when :failed + true + when :success + true unless notify_only_broken_builds? + else + false + end + end + + def execute(build) + msg = Ci::HipChatMessage.new(build) + opts = default_options.merge( + token: hipchat_token, + room: hipchat_room, + server: server_url, + color: msg.status_color, + notify: msg.notify? + ) + Ci::HipChatNotifierWorker.perform_async(msg.to_s, opts) + end + + private + + def default_options + { + service_name: 'GitLab CI', + message_format: 'html' + } + end + + def server_url + if hipchat_server.blank? + 'https://api.hipchat.com' + else + hipchat_server + end + end + end +end diff --git a/app/models/project_services/ci/mail_service.rb b/app/models/project_services/ci/mail_service.rb new file mode 100644 index 00000000000..1bd2f33612b --- /dev/null +++ b/app/models/project_services/ci/mail_service.rb @@ -0,0 +1,84 @@ +# == 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 +# + +module Ci + class MailService < Ci::Service + delegate :email_recipients, :email_recipients=, + :email_add_pusher, :email_add_pusher=, + :email_only_broken_builds, :email_only_broken_builds=, to: :project, prefix: false + + before_save :update_project + + default_value_for :active, true + + def title + 'Mail' + end + + def description + 'Email notification' + end + + def to_param + 'mail' + end + + def fields + [ + { type: 'text', name: 'email_recipients', label: 'Recipients', help: 'Whitespace-separated list of recipient addresses' }, + { type: 'checkbox', name: 'email_add_pusher', label: 'Add pusher to recipients list' }, + { type: 'checkbox', name: 'email_only_broken_builds', label: 'Notify only broken builds' } + ] + end + + def can_execute?(build) + return if build.allow_failure? + + # it doesn't make sense to send emails for retried builds + commit = build.commit + return unless commit + return unless commit.builds_without_retry.include?(build) + + case build.status.to_sym + when :failed + true + when :success + true unless email_only_broken_builds + else + false + end + end + + def execute(build) + build.commit.project_recipients.each do |recipient| + case build.status.to_sym + when :success + mailer.build_success_email(build.id, recipient) + when :failed + mailer.build_fail_email(build.id, recipient) + end + end + end + + private + + def update_project + project.save! + end + + def mailer + Ci::Notify.delay + end + end +end diff --git a/app/models/project_services/ci/slack_message.rb b/app/models/project_services/ci/slack_message.rb new file mode 100644 index 00000000000..757b1961143 --- /dev/null +++ b/app/models/project_services/ci/slack_message.rb @@ -0,0 +1,99 @@ +require 'slack-notifier' + +module Ci + class SlackMessage + include Gitlab::Application.routes.url_helpers + + def initialize(commit) + @commit = commit + end + + def pretext + '' + end + + def color + attachment_color + end + + def fallback + format(attachment_message) + end + + def attachments + fields = [] + + if commit.matrix? + commit.builds_without_retry.each do |build| + next if build.allow_failure? + next unless build.failed? + fields << { + title: build.name, + value: "Build <#{ci_project_build_url(project, build)}|\##{build.id}> failed in #{build.duration.to_i} second(s)." + } + end + end + + [{ + text: attachment_message, + color: attachment_color, + fields: fields + }] + end + + private + + attr_reader :commit + + def attachment_message + out = "<#{ci_project_url(project)}|#{project_name}>: " + if commit.matrix? + out << "Commit <#{ci_project_ref_commits_url(project, commit.ref, commit.sha)}|\##{commit.id}> " + else + build = commit.builds_without_retry.first + out << "Build <#{ci_project_build_url(project, build)}|\##{build.id}> " + end + out << "(<#{commit_sha_link}|#{commit.short_sha}>) " + out << "of <#{commit_ref_link}|#{commit.ref}> " + out << "by #{commit.git_author_name} " if commit.git_author_name + out << "#{commit_status} in " + out << "#{commit.duration} second(s)" + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def project + commit.project + end + + def project_name + project.name + end + + def commit_sha_link + "#{project.gitlab_url}/commit/#{commit.sha}" + end + + def commit_ref_link + "#{project.gitlab_url}/commits/#{commit.ref}" + end + + def attachment_color + if commit.success? + 'good' + else + 'danger' + end + end + + def commit_status + if commit.success? + 'succeeded' + else + 'failed' + end + end + end +end diff --git a/app/models/project_services/ci/slack_service.rb b/app/models/project_services/ci/slack_service.rb new file mode 100644 index 00000000000..76db573dc17 --- /dev/null +++ b/app/models/project_services/ci/slack_service.rb @@ -0,0 +1,81 @@ +# == 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 +# + +module Ci + class SlackService < Ci::Service + prop_accessor :webhook + boolean_accessor :notify_only_broken_builds + validates :webhook, presence: true, if: :activated? + + default_value_for :notify_only_broken_builds, true + + def title + 'Slack' + end + + def description + 'A team communication tool for the 21st century' + end + + def to_param + 'slack' + end + + def help + 'Visit https://www.slack.com/services/new/incoming-webhook. Then copy link and save project!' unless webhook.present? + end + + def fields + [ + { type: 'text', name: 'webhook', label: 'Webhook URL', placeholder: '' }, + { type: 'checkbox', name: 'notify_only_broken_builds', label: 'Notify only broken builds' } + ] + end + + def can_execute?(build) + return if build.allow_failure? + + commit = build.commit + return unless commit + return unless commit.builds_without_retry.include?(build) + + case commit.status.to_sym + when :failed + true + when :success + true unless notify_only_broken_builds? + else + false + end + end + + def execute(build) + message = Ci::SlackMessage.new(build.commit) + options = default_options.merge( + color: message.color, + fallback: message.fallback, + attachments: message.attachments + ) + Ci::SlackNotifierWorker.perform_async(webhook, message.pretext, options) + end + + private + + def default_options + { + username: 'GitLab CI' + } + end + end +end diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index 3e2b7faecdb..c73c4b058a1 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -26,7 +26,7 @@ class DroneCiService < CiService format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, if: :activated? validates :token, presence: true, - format: { with: /\A([A-Za-z0-9]+)\z/ }, if: :activated? + if: :activated? after_save :compose_service_hook, if: :activated? @@ -135,20 +135,6 @@ class DroneCiService < CiService commit_page(sha, ref) end - def builds_path - url = [drone_url, "#{project.namespace.path}/#{project.path}"] - - URI.join(*url).to_s - end - - def status_img_path - url = [drone_url, - "api/badges/#{project.namespace.path}/#{project.path}/status.svg", - "?branch=#{URI::encode(project.default_branch)}"] - - URI.join(*url).to_s - end - def title 'Drone CI' end diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index acbbc9935b6..436d4cfed81 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -19,22 +19,12 @@ # class GitlabCiService < CiService - API_PREFIX = "api/v1" - - prop_accessor :project_url, :token, :enable_ssl_verification - validates :project_url, - presence: true, - format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, if: :activated? - validates :token, - presence: true, - format: { with: /\A([A-Za-z0-9]+)\z/ }, if: :activated? + include Gitlab::Application.routes.url_helpers after_save :compose_service_hook, if: :activated? def compose_service_hook hook = service_hook || build_service_hook - hook.url = [project_url, "/build", "?token=#{token}"].join("") - hook.enable_ssl_verification = enable_ssl_verification hook.save end @@ -55,71 +45,53 @@ class GitlabCiService < CiService end end - service_hook.execute(data) + ci_project = Ci::Project.find_by(gitlab_id: project.id) + if ci_project + Ci::CreateCommitService.new.execute(ci_project, data) + end end - def commit_status_path(sha, ref) - URI::encode(project_url + "/refs/#{ref}/commits/#{sha}/status.json?token=#{token}") + def token + if project.gitlab_ci_project.present? + project.gitlab_ci_project.token + end end - def get_ci_build(sha, ref) - @ci_builds ||= {} - @ci_builds[sha] ||= HTTParty.get(commit_status_path(sha, ref), verify: false) + def get_ci_commit(sha, ref) + Ci::Project.find(project.gitlab_ci_project).commits.find_by_sha_and_ref!(sha, ref) end def commit_status(sha, ref) - response = get_ci_build(sha, ref) - - if response.code == 200 and response["status"] - response["status"] - else - :error - end - rescue Errno::ECONNREFUSED + get_ci_commit(sha, ref).status + rescue ActiveRecord::RecordNotFound :error end - def fork_registration(new_project, private_token) - params = { + def fork_registration(new_project, current_user) + params = OpenStruct.new({ id: new_project.id, - name_with_namespace: new_project.name_with_namespace, - path_with_namespace: new_project.path_with_namespace, - web_url: new_project.web_url, - default_branch: new_project.default_branch, - ssh_url_to_repo: new_project.ssh_url_to_repo - } - - HTTParty.post( - fork_registration_path, - body: { - project_id: project.id, - project_token: token, - private_token: private_token, - data: params }, - verify: false + default_branch: new_project.default_branch + }) + + ci_project = Ci::Project.find_by!(gitlab_id: project.id) + + Ci::CreateProjectService.new.execute( + current_user, + params, + ci_project ) end def commit_coverage(sha, ref) - response = get_ci_build(sha, ref) - - if response.code == 200 and response["coverage"] - response["coverage"] - end - rescue Errno::ECONNREFUSED - nil + get_ci_commit(sha, ref).coverage + rescue ActiveRecord::RecordNotFound + :error end def build_page(sha, ref) - URI::encode(project_url + "/refs/#{ref}/commits/#{sha}") - end - - def builds_path - project_url + "?ref=" + project.default_branch - end - - def status_img_path - project_url + "/status.png?ref=" + project.default_branch + if project.gitlab_ci_project.present? + ci_project_ref_commits_url(project.gitlab_ci_project, ref, sha) + end end def title @@ -135,11 +107,7 @@ class GitlabCiService < CiService end def fields - [ - { type: 'text', name: 'token', placeholder: 'GitLab CI project specific token' }, - { type: 'text', name: 'project_url', placeholder: 'http://ci.gitlabhq.com/projects/3' }, - { type: 'checkbox', name: 'enable_ssl_verification', title: "Enable SSL verification" } - ] + [] end private @@ -148,10 +116,6 @@ class GitlabCiService < CiService repository.blob_at(sha, '.gitlab-ci.yml') end - def fork_registration_path - project_url.sub(/projects\/\d*/, "#{API_PREFIX}/forks") - end - def repository project.repository end diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 0ebc0a3ba1a..9558292fea3 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -19,7 +19,7 @@ # class GitlabIssueTrackerService < IssueTrackerService - include Rails.application.routes.url_helpers + include Gitlab::Application.routes.url_helpers prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index bfa8fc7b860..35e30b1cb0b 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -19,7 +19,7 @@ # class JiraService < IssueTrackerService - include Rails.application.routes.url_helpers + include Gitlab::Application.routes.url_helpers prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 36d9874edd3..7cd5e892507 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -34,6 +34,12 @@ class SlackService < Service 'slack' end + def help + 'This service sends notifications to your Slack channel.<br/> + To setup this Service you need to create a new <b>"Incoming webhook"</b> in your Slack integration panel, + and enter the Webhook URL below.' + end + def fields [ { type: 'text', name: 'webhook', diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb index 33b113a2a27..3eed5c16e45 100644 --- a/app/models/sent_notification.rb +++ b/app/models/sent_notification.rb @@ -8,6 +8,7 @@ # noteable_type :string(255) # recipient_id :integer # commit_id :string(255) +# line_code :string(255) # reply_key :string(255) not null # @@ -21,13 +22,20 @@ class SentNotification < ActiveRecord::Base validates :noteable_id, presence: true, unless: :for_commit? validates :commit_id, presence: true, if: :for_commit? + validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true class << self + def reply_key + return nil unless Gitlab::IncomingEmail.enabled? + + SecureRandom.hex(16) + end + def for(reply_key) find_by(reply_key: reply_key) end - def record(noteable, recipient_id, reply_key) + def record(noteable, recipient_id, reply_key, params = {}) return unless reply_key noteable_id = nil @@ -38,7 +46,7 @@ class SentNotification < ActiveRecord::Base noteable_id = noteable.id end - create( + params.reverse_merge!( project: noteable.project, noteable_type: noteable.class.name, noteable_id: noteable_id, @@ -46,6 +54,14 @@ class SentNotification < ActiveRecord::Base recipient_id: recipient_id, reply_key: reply_key ) + + create(params) + end + + def record_note(note, recipient_id, reply_key, params = {}) + params[:line_code] = note.line_code + + record(note.noteable, recipient_id, reply_key, params) end end diff --git a/app/models/user.rb b/app/models/user.rb index bff8eeed96d..3879f3fd381 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -172,7 +172,7 @@ class User < ActiveRecord::Base # User's Dashboard preference # Note: When adding an option, it MUST go on the end of the array. - enum dashboard: [:projects, :stars] + enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity] # User's Project preference # Note: When adding an option, it MUST go on the end of the array. @@ -753,4 +753,13 @@ class User < ActiveRecord::Base def can_be_removed? !solo_owned_groups.present? end + + def ci_authorized_projects + @ci_authorized_projects ||= Ci::Project.where(gitlab_id: authorized_projects) + end + + def ci_authorized_runners + Ci::Runner.specific.includes(:runner_projects). + where(ci_runner_projects: { project_id: ci_authorized_projects } ) + end end diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb new file mode 100644 index 00000000000..0a1abf89a95 --- /dev/null +++ b/app/services/ci/create_commit_service.rb @@ -0,0 +1,50 @@ +module Ci + class CreateCommitService + def execute(project, params) + before_sha = params[:before] + sha = params[:checkout_sha] || params[:after] + origin_ref = params[:ref] + + unless origin_ref && sha.present? + return false + end + + ref = origin_ref.gsub(/\Arefs\/(tags|heads)\//, '') + + # Skip branch removal + if sha == Ci::Git::BLANK_SHA + return false + end + + commit = project.commits.find_by_sha_and_ref(sha, ref) + + # Create commit if not exists yet + unless commit + data = { + ref: ref, + sha: sha, + tag: origin_ref.start_with?('refs/tags/'), + before_sha: before_sha, + push_data: { + before: before_sha, + after: sha, + ref: ref, + user_name: params[:user_name], + user_email: params[:user_email], + repository: params[:repository], + commits: params[:commits], + total_commits_count: params[:total_commits_count], + ci_yaml_file: params[:ci_yaml_file] + } + } + + commit = project.commits.create(data) + end + + commit.update_committed! + commit.create_builds unless commit.builds.any? + + commit + end + end +end diff --git a/app/services/ci/create_project_service.rb b/app/services/ci/create_project_service.rb new file mode 100644 index 00000000000..f42babd2388 --- /dev/null +++ b/app/services/ci/create_project_service.rb @@ -0,0 +1,30 @@ +module Ci + class CreateProjectService + include Gitlab::Application.routes.url_helpers + + def execute(current_user, params, forked_project = nil) + @project = Ci::Project.parse(params) + + Ci::Project.transaction do + @project.save! + + gl_project = ::Project.find(@project.gitlab_id) + gl_project.build_missing_services + gl_project.gitlab_ci_service.update_attributes(active: true) + end + + if forked_project + # Copy settings + settings = forked_project.attributes.select do |attr_name, value| + ["public", "shared_runners_enabled", "allow_git_fetch"].include? attr_name + end + + @project.update(settings) + end + + Ci::EventService.new.create_project(current_user, @project) + + @project + end + end +end diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb new file mode 100644 index 00000000000..9bad09f2f54 --- /dev/null +++ b/app/services/ci/create_trigger_request_service.rb @@ -0,0 +1,17 @@ +module Ci + class CreateTriggerRequestService + def execute(project, trigger, ref, variables = nil) + commit = project.commits.where(ref: ref).last + return unless commit + + trigger_request = trigger.trigger_requests.create!( + commit: commit, + variables: variables + ) + + if commit.create_builds(trigger_request) + trigger_request + end + end + end +end diff --git a/app/services/ci/event_service.rb b/app/services/ci/event_service.rb new file mode 100644 index 00000000000..3f4e02dd26c --- /dev/null +++ b/app/services/ci/event_service.rb @@ -0,0 +1,31 @@ +module Ci + class EventService + def remove_project(user, project) + create( + description: "Project \"#{project.name}\" has been removed by #{user.username}", + user_id: user.id, + is_admin: true + ) + end + + def create_project(user, project) + create( + description: "Project \"#{project.name}\" has been created by #{user.username}", + user_id: user.id, + is_admin: true + ) + end + + def change_project_settings(user, project) + create( + project_id: project.id, + user_id: user.id, + description: "User \"#{user.username}\" updated projects settings" + ) + end + + def create(*args) + Ci::Event.create!(*args) + end + end +end diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb new file mode 100644 index 00000000000..b95835ba093 --- /dev/null +++ b/app/services/ci/image_for_build_service.rb @@ -0,0 +1,31 @@ +module Ci + class ImageForBuildService + def execute(project, params) + image_name = + if params[:sha] + commit = project.commits.find_by(sha: params[:sha]) + image_for_commit(commit) + elsif params[:ref] + commit = project.last_commit_for_ref(params[:ref]) + image_for_commit(commit) + else + 'build-unknown.svg' + end + + image_path = Rails.root.join('public/ci', image_name) + + OpenStruct.new( + path: image_path, + name: image_name + ) + end + + private + + def image_for_commit(commit) + return 'build-unknown.svg' unless commit + + 'build-' + commit.status + ".svg" + end + end +end diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb new file mode 100644 index 00000000000..71b61bbe389 --- /dev/null +++ b/app/services/ci/register_build_service.rb @@ -0,0 +1,40 @@ +module Ci + # This class responsible for assigning + # proper pending build to runner on runner API request + class RegisterBuildService + def execute(current_runner) + builds = Ci::Build.pending.unstarted + + builds = + if current_runner.shared? + # don't run projects which have not enables shared runners + builds.joins(commit: { gl_project: :gitlab_ci_project }).where(ci_projects: { shared_runners_enabled: true }) + else + # do run projects which are only assigned to this runner + builds.joins(:commit).where(ci_commits: { gl_project_id: current_runner.gl_projects_ids }) + end + + builds = builds.order('created_at ASC') + + build = builds.find do |build| + (build.tag_list - current_runner.tag_list).empty? + end + + + if build + # In case when 2 runners try to assign the same build, second runner will be declined + # with StateMachine::InvalidTransition in run! method. + build.with_lock do + build.runner_id = current_runner.id + build.save! + build.run! + end + end + + build + + rescue StateMachine::InvalidTransition + nil + end + end +end diff --git a/app/services/ci/test_hook_service.rb b/app/services/ci/test_hook_service.rb new file mode 100644 index 00000000000..3a17596aaeb --- /dev/null +++ b/app/services/ci/test_hook_service.rb @@ -0,0 +1,7 @@ +module Ci + class TestHookService + def execute(hook, current_user) + Ci::WebHookService.new.build_end(hook.project.commits.last.last_build) + end + end +end diff --git a/app/services/ci/web_hook_service.rb b/app/services/ci/web_hook_service.rb new file mode 100644 index 00000000000..87984b20fa1 --- /dev/null +++ b/app/services/ci/web_hook_service.rb @@ -0,0 +1,36 @@ +module Ci + class WebHookService + def build_end(build) + execute_hooks(build.project, build_data(build)) + end + + def execute_hooks(project, data) + project.web_hooks.each do |web_hook| + async_execute_hook(web_hook, data) + end + end + + def async_execute_hook(hook, data) + Sidekiq::Client.enqueue(Ci::WebHookWorker, hook.id, data) + end + + def build_data(build) + project = build.project + data = {} + data.merge!({ + build_id: build.id, + build_name: build.name, + build_status: build.status, + build_started_at: build.started_at, + build_finished_at: build.finished_at, + project_id: project.id, + project_name: project.name, + gitlab_url: project.gitlab_url, + ref: build.ref, + sha: build.sha, + before_sha: build.before_sha, + push_data: build.commit.push_data + }) + end + end +end diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index 70f642baaaa..bfe6a3dc4be 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -4,7 +4,10 @@ require 'securerandom' # and return Gitlab::CompareResult object that responds to commits and diffs class CompareService def execute(source_project, source_branch, target_project, target_branch) - source_sha = source_project.commit(source_branch).sha + source_commit = source_project.commit(source_branch) + return unless source_commit + + source_sha = source_commit.sha # If compare with other project we need to fetch ref first unless target_project == source_project diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index 103d6b0a08b..07fc77001a5 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -46,6 +46,10 @@ class EventCreateService create_record_event(milestone, current_user, Event::REOPENED) end + def destroy_milestone(milestone, current_user) + create_record_event(milestone, current_user, Event::DESTROYED) + end + def leave_note(note, current_user) create_record_event(note, current_user, Event::COMMENTED) end diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index 91d715b2d63..ffbb5993279 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -19,10 +19,12 @@ module Files end unless project.empty_repo? + @file_path.slice!(0) if @file_path.start_with?('/') + blob = repository.blob_at_branch(@current_branch, @file_path) if blob - raise_error("Your changes could not be committed, because file with such name exists") + raise_error("Your changes could not be committed because a file with the same name already exists") end end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 0a73244774a..8193b6e192d 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -55,6 +55,12 @@ class GitPushService @push_data = build_push_data(oldrev, newrev, ref) + # If CI was disabled but .gitlab-ci.yml file was pushed + # we enable CI automatically + if !project.gitlab_ci? && gitlab_ci_yaml?(newrev) + project.enable_ci(user) + end + EventCreateService.new.push(project, user, @push_data) project.execute_hooks(@push_data.dup, :push_hooks) project.execute_services(@push_data.dup, :push_hooks) @@ -143,4 +149,10 @@ class GitPushService def commit_user(commit) commit.author || user end + + def gitlab_ci_yaml?(sha) + @project.repository.blob_at(sha, '.gitlab-ci.yml') + rescue Rugged::ReferenceError + nil + end end diff --git a/app/services/milestones/destroy_service.rb b/app/services/milestones/destroy_service.rb new file mode 100644 index 00000000000..2414966505b --- /dev/null +++ b/app/services/milestones/destroy_service.rb @@ -0,0 +1,27 @@ +module Milestones + class DestroyService < Milestones::BaseService + def execute(milestone) + + Milestone.transaction do + update_params = { milestone: nil } + + milestone.issues.each do |issue| + Issues::UpdateService.new(project, current_user, update_params).execute(issue) + end + + milestone.merge_requests.each do |merge_request| + MergeRequests::UpdateService.new(project, current_user, update_params).execute(merge_request) + end + + event_service.destroy_milestone(milestone, current_user) + + Event.for_milestone_id(milestone.id).each do |event| + event.target_id = nil + event.save + end + + milestone.destroy + end + end + end +end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index b35aed005da..e54a13ed6c5 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -55,9 +55,7 @@ module Projects @project.save if @project.persisted? && !@project.import? - unless @project.create_repository - raise 'Failed to create repository' - end + raise 'Failed to create repository' unless @project.create_repository end end @@ -87,6 +85,8 @@ module Projects @project.build_missing_services + @project.create_labels + event_service.create_project(@project, current_user) system_hook_service.execute_hooks_for(@project, :create) diff --git a/app/services/projects/download_service.rb b/app/services/projects/download_service.rb new file mode 100644 index 00000000000..99f22293d0d --- /dev/null +++ b/app/services/projects/download_service.rb @@ -0,0 +1,43 @@ +module Projects + class DownloadService < BaseService + + WHITELIST = [ + /^[^.]+\.fogbugz.com$/ + ] + + def initialize(project, url) + @project, @url = project, url + end + + def execute + return nil unless valid_url?(@url) + + uploader = FileUploader.new(@project) + uploader.download!(@url) + uploader.store! + + filename = uploader.image? ? uploader.file.basename : uploader.file.filename + + { + 'alt' => filename, + 'url' => uploader.secure_url, + 'is_image' => uploader.image? + } + end + + private + + def valid_url?(url) + url && http?(url) && valid_domain?(url) + end + + def http?(url) + url =~ /\A#{URI::regexp(['http', 'https'])}\z/ + end + + def valid_domain?(url) + host = URI.parse(url).host + WHITELIST.any? { |entry| entry === host } + end + end +end diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 50f208b11d1..2e995d6fd51 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -18,7 +18,7 @@ module Projects if new_project.persisted? if @project.gitlab_ci? - ForkRegistrationWorker.perform_async(@project.id, new_project.id, @current_user.private_token) + @project.gitlab_ci_service.fork_registration(new_project, @current_user) end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 330b8bbf9d2..143cd10c543 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -118,6 +118,20 @@ .col-sm-10 = f.text_area :sign_in_text, class: 'form-control', rows: 4 .help-block Markdown enabled + .form-group + = f.label :help_page_text, class: 'control-label col-sm-2' + .col-sm-10 + = f.text_area :help_page_text, class: 'form-control', rows: 4 + .help-block Markdown enabled + + %fieldset + %legend Continuous Integration + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :ci_enabled do + = f.check_box :ci_enabled + Disable to prevent CI usage until rake ci:migrate is run (8.0 only) .form-actions = f.submit 'Save', class: 'btn btn-primary' diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml index fc921a966f3..f8cd98f0ec4 100644 --- a/app/views/admin/applications/index.html.haml +++ b/app/views/admin/applications/index.html.haml @@ -2,7 +2,7 @@ %h3.page-title System OAuth applications %p.light - System OAuth application does not belong to certain user and can be managed only by admins + System OAuth applications don't belong to any user and can only be managed by admins %hr %p= link_to 'New Application', new_admin_application_path, class: 'btn btn-success' %table.table.table-striped diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 54191aadda6..8657d2c71fe 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -58,7 +58,7 @@ %p Reply by email %span.light.pull-right - = boolean_to_icon Gitlab::ReplyByEmail.enabled? + = boolean_to_icon Gitlab::IncomingEmail.enabled? .col-md-4 %h4 Components diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml new file mode 100644 index 00000000000..ad58a3837f6 --- /dev/null +++ b/app/views/admin/labels/_form.html.haml @@ -0,0 +1,35 @@ += form_for [:admin, @label], html: { class: 'form-horizontal label-form js-requires-input' } do |f| + -if @label.errors.any? + .row + .col-sm-offset-2.col-sm-10 + .alert.alert-danger + - @label.errors.full_messages.each do |msg| + %span= msg + %br + + .form-group + = f.label :title, class: 'control-label' + .col-sm-10 + = f.text_field :title, class: "form-control", required: true + .form-group + = f.label :color, "Background Color", class: 'control-label' + .col-sm-10 + .input-group + .input-group-addon.label-color-preview + = f.color_field :color, class: "form-control" + .help-block + Choose any color. + %br + Or you can choose one of suggested colors below + + .suggest-colors + - suggested_colors.each do |color| + = link_to '#', style: "background-color: #{color}", data: { color: color } do + + + .form-actions + = f.submit 'Save', class: 'btn btn-save js-save-button' + = link_to "Cancel", admin_labels_path, class: 'btn btn-cancel' + +:coffeescript + new Labels diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml new file mode 100644 index 00000000000..596e06243dd --- /dev/null +++ b/app/views/admin/labels/_label.html.haml @@ -0,0 +1,5 @@ +%li{id: dom_id(label)} + = render_colored_label(label) + .pull-right + = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm' + = link_to 'Remove', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} diff --git a/app/views/admin/labels/destroy.js.haml b/app/views/admin/labels/destroy.js.haml new file mode 100644 index 00000000000..9d51762890f --- /dev/null +++ b/app/views/admin/labels/destroy.js.haml @@ -0,0 +1,2 @@ +- if @labels.size == 0 + $('.labels').load(document.URL + ' .light-well').hide().fadeIn(1000) diff --git a/app/views/admin/labels/edit.html.haml b/app/views/admin/labels/edit.html.haml new file mode 100644 index 00000000000..45c62a76259 --- /dev/null +++ b/app/views/admin/labels/edit.html.haml @@ -0,0 +1,9 @@ +- page_title "Edit", @label.name, "Labels" +%h3 + Edit label + %span.light #{@label.name} +.back-link + = link_to admin_labels_path do + ← To labels list +%hr += render 'form' diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml new file mode 100644 index 00000000000..d67454c03e7 --- /dev/null +++ b/app/views/admin/labels/index.html.haml @@ -0,0 +1,16 @@ +- page_title "Labels" += link_to new_admin_label_path, class: "pull-right btn btn-new" do + New label +%h3.page-title + Labels +%hr + +.labels + - if @labels.present? + %ul.bordered-list.manage-labels-list + = render @labels + = paginate @labels, theme: 'gitlab' + - else + .light-well + .nothing-here-block There are no labels yet + diff --git a/app/views/admin/labels/new.html.haml b/app/views/admin/labels/new.html.haml new file mode 100644 index 00000000000..8d298ad20f7 --- /dev/null +++ b/app/views/admin/labels/new.html.haml @@ -0,0 +1,7 @@ +- page_title "New Label" +%h3 New label +.back-link + = link_to admin_labels_path do + ← To labels list +%hr += render 'form' diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index 9d5e934c8ba..4245d0f1eda 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -6,6 +6,8 @@ %span.cred (Admin) .pull-right + - unless @user == current_user + = link_to 'Log in as this user', login_as_admin_user_path(@user), method: :post, class: "btn btn-grouped btn-info" = link_to edit_admin_user_path(@user), class: "btn btn-grouped" do %i.fa.fa-pencil-square-o Edit diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 5e40d95d1c5..e3698ac1c46 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -97,5 +97,5 @@ - if user.access_locked? = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success", data: { confirm: 'Are you sure?' } - if user.can_be_removed? - = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All tickets linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove" + = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove" = paginate @users, theme: "gitlab" diff --git a/app/views/ci/admin/application_settings/_form.html.haml b/app/views/ci/admin/application_settings/_form.html.haml new file mode 100644 index 00000000000..634c9daa477 --- /dev/null +++ b/app/views/ci/admin/application_settings/_form.html.haml @@ -0,0 +1,24 @@ += form_for @application_setting, url: ci_admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + - if @application_setting.errors.any? + #error_explanation + .alert.alert-danger + - @application_setting.errors.full_messages.each do |msg| + %p= msg + + %fieldset + %legend Default Project Settings + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :all_broken_builds do + = f.check_box :all_broken_builds + Send emails only on broken builds + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :add_pusher do + = f.check_box :add_pusher + Add pusher to recipients list + + .form-actions + = f.submit 'Save', class: 'btn btn-primary' diff --git a/app/views/ci/admin/application_settings/show.html.haml b/app/views/ci/admin/application_settings/show.html.haml new file mode 100644 index 00000000000..7ef0aa89ed6 --- /dev/null +++ b/app/views/ci/admin/application_settings/show.html.haml @@ -0,0 +1,3 @@ +%h3.page-title Settings +%hr += render 'form' diff --git a/app/views/ci/admin/builds/_build.html.haml b/app/views/ci/admin/builds/_build.html.haml new file mode 100644 index 00000000000..778d51d03be --- /dev/null +++ b/app/views/ci/admin/builds/_build.html.haml @@ -0,0 +1,32 @@ +- if build.commit && build.project + %tr.build + %td.build-link + = link_to ci_project_build_url(build.project, build) do + %strong #{build.id} + + %td.status + = ci_status_with_icon(build.status) + + %td.commit-link + = commit_link(build.commit) + + %td.runner + - if build.runner + = link_to build.runner.id, ci_admin_runner_path(build.runner) + + %td.build-project + = truncate build.project.name, length: 30 + + %td.build-message + %span= truncate(build.commit.git_commit_message, length: 30) + + %td.build-branch + %span= truncate(build.ref, length: 25) + + %td.duration + - if build.duration + #{duration_in_words(build.finished_at, build.started_at)} + + %td.timestamp + - if build.finished_at + %span #{time_ago_in_words build.finished_at} ago diff --git a/app/views/ci/admin/builds/index.html.haml b/app/views/ci/admin/builds/index.html.haml new file mode 100644 index 00000000000..d23119162cc --- /dev/null +++ b/app/views/ci/admin/builds/index.html.haml @@ -0,0 +1,28 @@ +%ul.nav.nav-tabs.append-bottom-20 + %li{class: ("active" if @scope.nil?)} + = link_to 'All builds', ci_admin_builds_path + + %li{class: ("active" if @scope == "pending")} + = link_to "Pending", ci_admin_builds_path(scope: :pending) + + %li{class: ("active" if @scope == "running")} + = link_to "Running", ci_admin_builds_path(scope: :running) + + +%table.builds + %thead + %tr + %th Build + %th Status + %th Commit + %th Runner + %th Project + %th Message + %th Branch + %th Duration + %th Finished at + + - @builds.each do |build| + = render "ci/admin/builds/build", build: build + += paginate @builds diff --git a/app/views/ci/admin/events/index.html.haml b/app/views/ci/admin/events/index.html.haml new file mode 100644 index 00000000000..f9ab0994304 --- /dev/null +++ b/app/views/ci/admin/events/index.html.haml @@ -0,0 +1,17 @@ +%table.table + %thead + %tr + %th User ID + %th Description + %th When + - @events.each do |event| + %tr + %td + = event.user_id + %td + = event.description + %td.light + = time_ago_in_words event.updated_at + ago + += paginate @events
\ No newline at end of file diff --git a/app/views/ci/admin/projects/_project.html.haml b/app/views/ci/admin/projects/_project.html.haml new file mode 100644 index 00000000000..a342d6e1cf0 --- /dev/null +++ b/app/views/ci/admin/projects/_project.html.haml @@ -0,0 +1,29 @@ +- last_commit = project.commits.last +%tr + %td + = project.id + %td + = link_to [:ci, project] do + %strong= project.name + %td + - if last_commit + = ci_status_with_icon(last_commit.status) + - if project.last_commit_date + · + = time_ago_in_words project.last_commit_date + ago + - else + No builds yet + %td + - if project.public + %i.fa.fa-globe + Public + - else + %i.fa.fa-lock + Private + %td + = project.commits.count + %td + = link_to [:ci, :admin, project], method: :delete, class: 'btn btn-danger btn-sm' do + %i.fa.fa-remove + Remove diff --git a/app/views/ci/admin/projects/index.html.haml b/app/views/ci/admin/projects/index.html.haml new file mode 100644 index 00000000000..dc7b041473b --- /dev/null +++ b/app/views/ci/admin/projects/index.html.haml @@ -0,0 +1,15 @@ +%table.table + %thead + %tr + %th ID + %th Name + %th Last build + %th Access + %th Builds + %th + + - @projects.each do |project| + = render "ci/admin/projects/project", project: project + += paginate @projects + diff --git a/app/views/ci/admin/runner_projects/index.html.haml b/app/views/ci/admin/runner_projects/index.html.haml new file mode 100644 index 00000000000..f049b4f4c4e --- /dev/null +++ b/app/views/ci/admin/runner_projects/index.html.haml @@ -0,0 +1,57 @@ +%p.lead + To register new runner visit #{link_to 'this page ', ci_runners_path} + +.row + .col-md-8 + %h5 Activated: + %table.table + %tr + %th Runner ID + %th Runner Description + %th Last build + %th Builds Stats + %th Registered + %th + + - @runner_projects.each do |runner_project| + - runner = runner_project.runner + - builds = runner.builds.where(project_id: @project.id) + %tr + %td + %span.badge.badge-info= runner.id + %td + = runner.display_name + %td + - last_build = builds.last + - if last_build + = link_to last_build.short_sha, [last_build.project, last_build] + - else + unknown + %td + %span.badge.badge-success + #{builds.success.count} + %span / + %span.badge.badge-important + #{builds.failed.count} + %td + #{time_ago_in_words(runner_project.created_at)} ago + %td + = link_to 'Disable', [:ci, @project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm right' + .col-md-4 + %h5 Available + %table.table + %tr + %th ID + %th Token + %th + + - (Ci::Runner.all - @project.runners).each do |runner| + %tr + %td + = runner.id + %td + = runner.token + %td + = form_for [:ci, @project, @runner_project] do |f| + = f.hidden_field :runner_id, value: runner.id + = f.submit 'Add', class: 'btn btn-sm' diff --git a/app/views/ci/admin/runners/_runner.html.haml b/app/views/ci/admin/runners/_runner.html.haml new file mode 100644 index 00000000000..701782d26bb --- /dev/null +++ b/app/views/ci/admin/runners/_runner.html.haml @@ -0,0 +1,48 @@ +%tr{id: dom_id(runner)} + %td + - if runner.shared? + %span.label.label-success shared + - else + %span.label.label-info specific + - unless runner.active? + %span.label.label-danger paused + + %td + = link_to ci_admin_runner_path(runner) do + = runner.short_sha + %td + .runner-description + = runner.description + %span (#{link_to 'edit', '#', class: 'edit-runner-link'}) + .runner-description-form.hide + = form_for [:ci, :admin, runner], remote: true, html: { class: 'form-inline' } do |f| + .form-group + = f.text_field :description, class: 'form-control' + = f.submit 'Save', class: 'btn' + %span (#{link_to 'cancel', '#', class: 'cancel'}) + %td + - if runner.shared? + \- + - else + = runner.projects.count(:all) + %td + #{runner.builds.count(:all)} + %td + - runner.tag_list.each do |tag| + %span.label.label-primary + = tag + %td + - if runner.contacted_at + #{time_ago_in_words(runner.contacted_at)} ago + - else + Never + %td + .pull-right + = link_to 'Edit', ci_admin_runner_path(runner), class: 'btn btn-sm' + + - if runner.active? + = link_to 'Pause', [:pause, :ci, :admin, runner], data: { confirm: "Are you sure?" }, method: :get, class: 'btn btn-danger btn-sm' + - else + = link_to 'Resume', [:resume, :ci, :admin, runner], method: :get, class: 'btn btn-success btn-sm' + = link_to 'Remove', [:ci, :admin, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' + diff --git a/app/views/ci/admin/runners/index.html.haml b/app/views/ci/admin/runners/index.html.haml new file mode 100644 index 00000000000..b9d6703ff41 --- /dev/null +++ b/app/views/ci/admin/runners/index.html.haml @@ -0,0 +1,52 @@ +%p.lead + %span To register new runner you should enter the following registration token. With this token the runner will request a unique runner token and use that for future communication. + %code #{GitlabCi::REGISTRATION_TOKEN} + +.bs-callout + %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. + %br + + %div + %span Each runner can be in one of the following states: + %ul + %li + %span.label.label-success shared + \- run builds from all unassigned projects + %li + %span.label.label-info specific + \- run builds from assigned projects + %li + %span.label.label-danger paused + \- runner will not receive any new build + +.append-bottom-20.clearfix + .pull-left + = form_tag ci_admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do + .form-group + = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token' + = submit_tag 'Search', class: 'btn' + + .pull-right.light + Runners with last contact less than a minute ago: #{@active_runners_cnt} + +%br + +%table.table + %thead + %tr + %th Type + %th Runner token + %th Description + %th Projects + %th Builds + %th Tags + %th Last contact + %th + + - @runners.each do |runner| + = render "ci/admin/runners/runner", runner: runner += paginate @runners diff --git a/app/views/ci/admin/runners/show.html.haml b/app/views/ci/admin/runners/show.html.haml new file mode 100644 index 00000000000..09905e0eb47 --- /dev/null +++ b/app/views/ci/admin/runners/show.html.haml @@ -0,0 +1,118 @@ += 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 + + + +- if @runner.shared? + .bs-callout.bs-callout-success + %h4 This runner will process build from ALL UNASSIGNED projects + %p + If you want runners to build only specific projects, enable them in the table below. + Keep in mind that this is a one way transition. +- else + .bs-callout.bs-callout-info + %h4 This runner will process build only from ASSIGNED projects + %p You can't make this a shared runner. +%hr += form_for @runner, url: ci_admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f| + .form-group + = label_tag :token, class: 'control-label' do + Token + .col-sm-10 + = f.text_field :token, class: 'form-control', readonly: true + .form-group + = label_tag :description, class: 'control-label' do + Description + .col-sm-10 + = f.text_field :description, class: 'form-control' + .form-group + = label_tag :tag_list, class: 'control-label' do + Tags + .col-sm-10 + = f.text_field :tag_list, class: 'form-control' + .help-block You can setup builds to only use runners with specific tags + .form-actions + = f.submit 'Save', class: 'btn btn-save' + +.row + .col-md-6 + %h4 Restrict projects for this runner + - if @runner.projects.any? + %table.table + %thead + %tr + %th Assigned projects + %th + - @runner.runner_projects.each do |runner_project| + - project = runner_project.project + %tr.alert-info + %td + %strong + = project.name + %td + .pull-right + = link_to 'Disable', [:ci, :admin, project, runner_project], method: :delete, class: 'btn btn-danger btn-xs' + + %table.table + %thead + %tr + %th Project + %th + .pull-right + = link_to 'Assign to all', assign_all_ci_admin_runner_path(@runner), + class: 'btn btn-sm assign-all-runner', + title: 'Assign runner to all projects', + method: :put + + %tr + %td + = form_tag ci_admin_runner_path(@runner), id: 'runner-projects-search', class: 'form-inline', method: :get do + .form-group + = search_field_tag :search, params[:search], class: 'form-control' + = submit_tag 'Search', class: 'btn' + + %td + - @projects.each do |project| + %tr + %td + = project.name + %td + .pull-right + = form_for [:ci, :admin, project, project.runner_projects.new] do |f| + = f.hidden_field :runner_id, value: @runner.id + = f.submit 'Enable', class: 'btn btn-xs' + = paginate @projects + + .col-md-6 + %h4 Recent builds served by this runner + %table.builds.runner-builds + %thead + %tr + %th Status + %th Project + %th Commit + %th Finished at + + - @builds.each do |build| + %tr.build + %td.status + = ci_status_with_icon(build.status) + + %td.status + = build.project.name + + %td.build-link + = link_to ci_project_build_path(build.project, build) do + %strong #{build.short_sha} + + %td.timestamp + - if build.finished_at + %span #{time_ago_in_words build.finished_at} ago diff --git a/app/views/ci/admin/runners/update.js.haml b/app/views/ci/admin/runners/update.js.haml new file mode 100644 index 00000000000..2b7d3067e20 --- /dev/null +++ b/app/views/ci/admin/runners/update.js.haml @@ -0,0 +1,2 @@ +:plain + $("#runner_#{@runner.id}").replaceWith("#{escape_javascript(render(@runner))}") diff --git a/app/views/ci/builds/_build.html.haml b/app/views/ci/builds/_build.html.haml new file mode 100644 index 00000000000..515b862e992 --- /dev/null +++ b/app/views/ci/builds/_build.html.haml @@ -0,0 +1,45 @@ +%tr.build + %td.status + = ci_status_with_icon(build.status) + + %td.build-link + = link_to ci_project_build_path(build.project, build) do + %strong Build ##{build.id} + + %td + = build.stage + + %td + = build.name + .pull-right + - if build.tags.any? + - build.tag_list.each do |tag| + %span.label.label-primary + = tag + - if build.trigger_request + %span.label.label-info triggered + - if build.allow_failure + %span.label.label-danger allowed to fail + + %td.duration + - if build.duration + #{duration_in_words(build.finished_at, build.started_at)} + + %td.timestamp + - if build.finished_at + %span #{time_ago_in_words build.finished_at} ago + + - if build.project.coverage_enabled? + %td.coverage + - if build.coverage + #{build.coverage}% + + %td + - if defined?(controls) && current_user && can?(current_user, :manage_builds, gl_project) + .pull-right + - if build.active? + = link_to cancel_ci_project_build_path(build.project, build, return_to: request.original_url), title: 'Cancel build' do + %i.fa.fa-remove.cred + - elsif build.commands.present? + = link_to retry_ci_project_build_path(build.project, build, return_to: request.original_url), method: :post, title: 'Retry build' do + %i.fa.fa-repeat diff --git a/app/views/ci/builds/show.html.haml b/app/views/ci/builds/show.html.haml new file mode 100644 index 00000000000..839dbf5c554 --- /dev/null +++ b/app/views/ci/builds/show.html.haml @@ -0,0 +1,170 @@ +#up-build-trace +- if @commit.matrix? + %ul.center-top-menu + - @commit.builds_without_retry_sorted.each do |build| + %li{class: ('active' if build == @build) } + = link_to ci_project_build_url(@project, build) do + = ci_icon_for_status(build.status) + %span + - if build.name + = build.name + - else + = build.id + + + - unless @commit.builds_without_retry.include?(@build) + %li.active + %a + Build ##{@build.id} + · + %i.fa.fa-warning-sign + This build was retried. + +.gray-content-block + .build-head + %h4 + - if @build.commit.tag? + Build for tag + %code #{@build.ref} + - else + Build for commit + %strong.monospace= commit_link(@build.commit) + from + + = link_to ci_project_path(@build.project, ref: @build.ref) do + %strong.monospace= "#{@build.ref}" + + - if @build.duration + .pull-right + %span + %i.fa.fa-time + #{duration_in_words(@build.finished_at, @build.started_at)} + + .clearfix + = ci_status_with_icon(@build.status) + .pull-right + = @build.updated_at.stamp('19:00 Aug 27') + +.row.prepend-top-default + .col-md-9 + .clearfix + - if @build.active? + .autoscroll-container + %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll + .clearfix + .scroll-controls + = link_to '#up-build-trace', class: 'btn' do + %i.fa.fa-angle-up + = link_to '#down-build-trace', class: 'btn' do + %i.fa.fa-angle-down + + %pre.trace#build-trace + %code.bash + = preserve do + = raw @build.trace_html + %div#down-build-trace + + .col-md-3 + - if @build.coverage + .build-widget + %h4.title + Test coverage + %h1 #{@build.coverage}% + + + .build-widget + %h4.title + Build + - if current_user && can?(current_user, :manage_builds, gl_project) + .pull-right + - if @build.active? + = link_to "Cancel", cancel_ci_project_build_path(@project, @build), class: 'btn btn-sm btn-danger' + - elsif @build.commands.present? + = link_to "Retry", retry_ci_project_build_path(@project, @build), class: 'btn btn-sm btn-primary', method: :post + + - if @build.duration + %p + %span.attr-name Duration: + #{duration_in_words(@build.finished_at, @build.started_at)} + %p + %span.attr-name Created: + #{time_ago_in_words(@build.created_at)} ago + - if @build.finished_at + %p + %span.attr-name Finished: + #{time_ago_in_words(@build.finished_at)} ago + %p + %span.attr-name Runner: + - if @build.runner && current_user && current_user.admin + \#{link_to "##{@build.runner.id}", ci_admin_runner_path(@build.runner.id)} + - elsif @build.runner + \##{@build.runner.id} + + - if @build.trigger_request + .build-widget + %h4.title + Trigger + + %p + %span.attr-name Token: + #{@build.trigger_request.trigger.short_token} + + - if @build.trigger_request.variables + %p + %span.attr-name Variables: + + %code + - @build.trigger_request.variables.each do |key, value| + #{key}=#{value} + + .build-widget + %h4.title + Commit + .pull-right + %small #{build_commit_link @build} + + - if @build.commit.compare? + %p + %span.attr-name Compare: + #{build_compare_link @build} + %p + %span.attr-name Branch: + #{build_ref_link @build} + %p + %span.attr-name Author: + #{@build.commit.git_author_name} + %p + %span.attr-name Message: + #{@build.commit.git_commit_message} + + - if @build.tags.any? + .build-widget + %h4.title + Tags + - @build.tag_list.each do |tag| + %span.label.label-primary + = tag + + - if @builds.present? + .build-widget + %h4.title #{pluralize(@builds.count, "other build")} for #{@build.short_sha}: + %table.builds + - @builds.each_with_index do |build, i| + %tr.build + %td + = ci_icon_for_status(build.status) + %td + = link_to ci_project_build_url(@project, build) do + - if build.name + = build.name + - else + %span ##{build.id} + + %td.status= build.status + + + = paginate @builds + + +:javascript + new CiBuild("#{ci_project_build_url(@project, @build)}", "#{@build.status}") diff --git a/app/views/ci/commits/_commit.html.haml b/app/views/ci/commits/_commit.html.haml new file mode 100644 index 00000000000..1eacfca944f --- /dev/null +++ b/app/views/ci/commits/_commit.html.haml @@ -0,0 +1,32 @@ +%tr.build + %td.status + = ci_status_with_icon(commit.status) + - if commit.running? + · + = commit.stage + + + %td.build-link + = link_to ci_project_ref_commits_path(commit.project, commit.ref, commit.sha) do + %strong #{commit.short_sha} + + %td.build-message + %span= truncate_first_line(commit.git_commit_message) + + %td.build-branch + - unless @ref + %span + = link_to truncate(commit.ref, length: 25), ci_project_path(@project, ref: commit.ref) + + %td.duration + - if commit.duration > 0 + #{time_interval_in_words commit.duration} + + %td.timestamp + - if commit.finished_at + %span #{time_ago_in_words commit.finished_at} ago + + - if commit.project.coverage_enabled? + %td.coverage + - if commit.coverage + #{commit.coverage}% diff --git a/app/views/ci/commits/show.html.haml b/app/views/ci/commits/show.html.haml new file mode 100644 index 00000000000..8f38aa84676 --- /dev/null +++ b/app/views/ci/commits/show.html.haml @@ -0,0 +1,87 @@ +.commit-info + .append-bottom-20 + = ci_status_with_icon(@commit.status) + + .gray-content-block.middle-block + %pre.commit-message + #{@commit.git_commit_message} + + .gray-content-block.second-block + .row + .col-sm-6 + - if @commit.compare? + %p + %span.attr-name Compare: + #{gitlab_compare_link(@project, @commit.short_before_sha, @commit.short_sha)} + - else + %p + %span.attr-name Commit: + #{gitlab_commit_link(@project, @commit.sha)} + + %p + %span.attr-name Branch: + #{gitlab_ref_link(@project, @commit.ref)} + .col-sm-6 + %p + %span.attr-name Author: + #{@commit.git_author_name} (#{@commit.git_author_email}) + - if @commit.created_at + %p + %span.attr-name Created at: + #{@commit.created_at.to_s(:short)} + +- if current_user && can?(current_user, :manage_builds, gl_project) + .pull-right + - if @commit.builds.running_or_pending.any? + = link_to "Cancel", cancel_ci_project_ref_commits_path(@project, @commit.ref, @commit.sha), class: 'btn btn-sm btn-danger' + + +- if @commit.yaml_errors.present? + .bs-callout.bs-callout-danger + %h4 Found errors in your .gitlab-ci.yml: + %ul + - @commit.yaml_errors.split(",").each do |error| + %li= error + +- unless @commit.push_data[:ci_yaml_file] + .bs-callout.bs-callout-warning + \.gitlab-ci.yml not found in this commit + +%h3 + Builds + - if @commit.duration > 0 + %small.pull-right + %i.fa.fa-time + #{time_interval_in_words @commit.duration} + +%table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Stage + %th Name + %th Duration + %th Finished at + - if @project.coverage_enabled? + %th Coverage + %th + = render @commit.builds_without_retry_sorted, controls: true + +- if @commit.retried_builds.any? + %h3 + Retried builds + + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Stage + %th Name + %th Duration + %th Finished at + - if @project.coverage_enabled? + %th Coverage + %th + = render @commit.retried_builds diff --git a/app/views/ci/errors/show.haml b/app/views/ci/errors/show.haml new file mode 100644 index 00000000000..2788112c835 --- /dev/null +++ b/app/views/ci/errors/show.haml @@ -0,0 +1,2 @@ +%h3.error Error += @error diff --git a/app/views/ci/events/index.html.haml b/app/views/ci/events/index.html.haml new file mode 100644 index 00000000000..779f49b3d3a --- /dev/null +++ b/app/views/ci/events/index.html.haml @@ -0,0 +1,19 @@ +%h3.page-title Events + +%table.table + %thead + %tr + %th User ID + %th Description + %th When + - @events.each do |event| + %tr + %td + = event.user_id + %td + = event.description + %td.light + = time_ago_in_words event.updated_at + ago + += paginate @events
\ No newline at end of file diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml new file mode 100644 index 00000000000..e2179e60f3e --- /dev/null +++ b/app/views/ci/lints/_create.html.haml @@ -0,0 +1,39 @@ +- if @status + %p + %b Status: + syntax is correct + %i.fa.fa-ok.correct-syntax + + %table.table.table-bordered + %thead + %tr + %th Parameter + %th Value + %tbody + - @stages.each do |stage| + - @builds.select { |build| build[:stage] == stage }.each do |build| + %tr + %td #{stage.capitalize} Job - #{build[:name]} + %td + %pre + = simple_format build[:script] + + %br + %b Tag list: + = build[:tags] + %br + %b Refs only: + = build[:only] && build[:only].join(", ") + %br + %b Refs except: + = build[:except] && build[:except].join(", ") + +-else + %p + %b Status: + syntax is incorrect + %i.fa.fa-remove.incorrect-syntax + %b Error: + = @error + + diff --git a/app/views/ci/lints/create.js.haml b/app/views/ci/lints/create.js.haml new file mode 100644 index 00000000000..a96c0b11b6e --- /dev/null +++ b/app/views/ci/lints/create.js.haml @@ -0,0 +1,2 @@ +:plain + $(".results").html("#{escape_javascript(render "create")}")
\ No newline at end of file diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml new file mode 100644 index 00000000000..a9b954771c5 --- /dev/null +++ b/app/views/ci/lints/show.html.haml @@ -0,0 +1,25 @@ +%h2 Check your .gitlab-ci.yml +%hr + += form_tag ci_lint_path, method: :post, remote: true do + .control-group + = label_tag :content, "Content of .gitlab-ci.yml", class: 'control-label' + .controls + = text_area_tag :content, nil, class: 'form-control span1', rows: 7, require: true + + .control-group.clearfix + .controls.pull-left.prepend-top-10 + = submit_tag "Validate", class: 'btn btn-success submit-yml' + + +%p.text-center.loading + %i.fa.fa-refresh.fa-spin + +.results.prepend-top-20 + +:coffeescript + $(".loading").hide() + $('form').bind 'ajax:beforeSend', -> + $(".loading").show() + $('form').bind 'ajax:complete', -> + $(".loading").hide() diff --git a/app/views/ci/notify/build_fail_email.html.haml b/app/views/ci/notify/build_fail_email.html.haml new file mode 100644 index 00000000000..d818e8b6756 --- /dev/null +++ b/app/views/ci/notify/build_fail_email.html.haml @@ -0,0 +1,19 @@ +- content_for :header do + %h1{style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"} + GitLab CI (build failed) +%h3 + Project: + = link_to ci_project_url(@project) do + = @project.name + +%p + Commit link: #{gitlab_commit_link(@project, @build.commit.short_sha)} +%p + Author: #{@build.commit.git_author_name} +%p + Branch: #{@build.commit.ref} +%p + Message: #{@build.commit.git_commit_message} + +%p + Url: #{link_to @build.short_sha, ci_project_build_url(@project, @build)} diff --git a/app/views/ci/notify/build_fail_email.text.erb b/app/views/ci/notify/build_fail_email.text.erb new file mode 100644 index 00000000000..1add215a1c8 --- /dev/null +++ b/app/views/ci/notify/build_fail_email.text.erb @@ -0,0 +1,9 @@ +Build failed for <%= @project.name %> + +Status: <%= @build.status %> +Commit: <%= @build.commit.short_sha %> +Author: <%= @build.commit.git_author_name %> +Branch: <%= @build.commit.ref %> +Message: <%= @build.commit.git_commit_message %> + +Url: <%= ci_project_build_url(@build.project, @build) %> diff --git a/app/views/ci/notify/build_success_email.html.haml b/app/views/ci/notify/build_success_email.html.haml new file mode 100644 index 00000000000..a20dcaee24e --- /dev/null +++ b/app/views/ci/notify/build_success_email.html.haml @@ -0,0 +1,20 @@ +- content_for :header do + %h1{style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"} + GitLab CI (build successful) + +%h3 + Project: + = link_to ci_project_url(@project) do + = @project.name + +%p + Commit link: #{gitlab_commit_link(@project, @build.commit.short_sha)} +%p + Author: #{@build.commit.git_author_name} +%p + Branch: #{@build.commit.ref} +%p + Message: #{@build.commit.git_commit_message} + +%p + Url: #{link_to @build.short_sha, ci_project_build_url(@project, @build)} diff --git a/app/views/ci/notify/build_success_email.text.erb b/app/views/ci/notify/build_success_email.text.erb new file mode 100644 index 00000000000..7ebd17e7270 --- /dev/null +++ b/app/views/ci/notify/build_success_email.text.erb @@ -0,0 +1,9 @@ +Build successful for <%= @project.name %> + +Status: <%= @build.status %> +Commit: <%= @build.commit.short_sha %> +Author: <%= @build.commit.git_author_name %> +Branch: <%= @build.commit.ref %> +Message: <%= @build.commit.git_commit_message %> + +Url: <%= ci_project_build_url(@build.project, @build) %> diff --git a/app/views/ci/projects/_info.html.haml b/app/views/ci/projects/_info.html.haml new file mode 100644 index 00000000000..1888e1bde93 --- /dev/null +++ b/app/views/ci/projects/_info.html.haml @@ -0,0 +1,2 @@ +- if no_runners_for_project?(@project) + = render 'no_runners' diff --git a/app/views/ci/projects/_no_runners.html.haml b/app/views/ci/projects/_no_runners.html.haml new file mode 100644 index 00000000000..c0a296fb17d --- /dev/null +++ b/app/views/ci/projects/_no_runners.html.haml @@ -0,0 +1,8 @@ +.alert.alert-danger + %p + There are NO runners to build this project. + %br + You can add Specific runner for this project on Runners page + + - if current_user.is_admin + or add Shared runner for whole application in admin are. diff --git a/app/views/ci/projects/disabled.html.haml b/app/views/ci/projects/disabled.html.haml new file mode 100644 index 00000000000..83b0d8329e1 --- /dev/null +++ b/app/views/ci/projects/disabled.html.haml @@ -0,0 +1 @@ +Continuous Integration has been disabled for time of the migration. diff --git a/app/views/ci/projects/show.html.haml b/app/views/ci/projects/show.html.haml new file mode 100644 index 00000000000..73e60795ba6 --- /dev/null +++ b/app/views/ci/projects/show.html.haml @@ -0,0 +1,60 @@ += render 'ci/shared/guide' unless @project.setup_finished? + +- if current_user && can?(current_user, :manage_project, gl_project) && !@project.any_runners? + .alert.alert-danger + Builds for this project wont be served unless you configure runners on + = link_to "Runners page", runners_path(@project.gl_project) + +%ul.nav.nav-tabs.append-bottom-20 + %li{class: ref_tab_class} + = link_to 'All commits', ci_project_path(@project) + - @project.tracked_refs.each do |ref| + %li{class: ref_tab_class(ref)} + = link_to ref, ci_project_path(@project, ref: ref) + + - if @ref && !@project.tracked_refs.include?(@ref) + %li{class: 'active'} + = link_to @ref, ci_project_path(@project, ref: @ref) + + %li.pull-right + = link_to 'View on GitLab', @project.gitlab_url, no_turbolink.merge( class: 'btn btn-sm' ) + +- if @ref + %p + Paste build status image for #{@ref} with next link + = link_to '#', class: 'badge-codes-toggle btn btn-default btn-xs' do + Status Badge + .badge-codes-block.bs-callout.bs-callout-info.hide + %p + Status badge for + %span.label.label-info #{@ref} + branch + %div + %label Markdown: + = text_field_tag 'badge_md', markdown_badge_code(@project, @ref), readonly: true, class: 'form-control' + %label Html: + = text_field_tag 'badge_html', html_badge_code(@project, @ref), readonly: true, class: 'form-control' + + + + +%table.table.builds + %thead + %tr + %th Status + %th Commit + %th Message + %th Branch + %th Total duration + %th Finished at + - if @project.coverage_enabled? + %th Coverage + + = render @commits + += paginate @commits + +- if @commits.empty? + .bs-callout + %h4 No commits yet + diff --git a/app/views/ci/services/_form.html.haml b/app/views/ci/services/_form.html.haml new file mode 100644 index 00000000000..9110aaa0528 --- /dev/null +++ b/app/views/ci/services/_form.html.haml @@ -0,0 +1,57 @@ +%h3.page-title + = @service.title + = boolean_to_icon @service.activated? + +%p= @service.description + +.back-link + = link_to ci_project_services_path(@project) do + ← to services + +%hr + += form_for(@service, as: :service, url: ci_project_service_path(@project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f| + - if @service.errors.any? + .alert.alert-danger + %ul + - @service.errors.full_messages.each do |msg| + %li= msg + + - if @service.help.present? + .bs-callout + = @service.help + + .form-group + = f.label :active, "Active", class: "control-label" + .col-sm-10 + = f.check_box :active + + - @service.fields.each do |field| + - name = field[:name] + - label = field[:label] || name + - value = @service.send(name) + - type = field[:type] + - placeholder = field[:placeholder] + - choices = field[:choices] + - default_choice = field[:default_choice] + - help = field[:help] + + .form-group + = f.label label, class: "control-label" + .col-sm-10 + - if type == 'text' + = f.text_field name, class: "form-control", placeholder: placeholder + - elsif type == 'textarea' + = f.text_area name, rows: 5, class: "form-control", placeholder: placeholder + - elsif type == 'checkbox' + = f.check_box name + - elsif type == 'select' + = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } + - if help + .light #{help} + + .form-actions + = f.submit 'Save', class: 'btn btn-save' + + - if @service.valid? && @service.activated? && @service.can_test? + = link_to 'Test settings', test_ci_project_service_path(@project, @service.to_param), class: 'btn' diff --git a/app/views/ci/services/edit.html.haml b/app/views/ci/services/edit.html.haml new file mode 100644 index 00000000000..bcc5832792f --- /dev/null +++ b/app/views/ci/services/edit.html.haml @@ -0,0 +1 @@ += render 'form' diff --git a/app/views/ci/services/index.html.haml b/app/views/ci/services/index.html.haml new file mode 100644 index 00000000000..37e5723b541 --- /dev/null +++ b/app/views/ci/services/index.html.haml @@ -0,0 +1,22 @@ +%h3.page-title Project services +%p.light Project services allow you to integrate GitLab CI with other applications + +%table.table + %thead + %tr + %th + %th Service + %th Desription + %th Last edit + - @services.sort_by(&:title).each do |service| + %tr + %td + = boolean_to_icon service.activated? + %td + = link_to edit_ci_project_service_path(@project, service.to_param) do + %strong= service.title + %td + = service.description + %td.light + = time_ago_in_words service.updated_at + ago diff --git a/app/views/ci/shared/_guide.html.haml b/app/views/ci/shared/_guide.html.haml new file mode 100644 index 00000000000..db2d7f2f4b6 --- /dev/null +++ b/app/views/ci/shared/_guide.html.haml @@ -0,0 +1,15 @@ +.bs-callout.help-callout + %h4 How to setup CI for this project + + %ol + %li + Add at least one runner to the project. + Go to #{link_to 'Runners page', runners_path(@project.gl_project), target: :blank} for instructions. + %li + Put the .gitlab-ci.yml in the root of your repository. Examples can be found in #{link_to "Configuring project (.gitlab-ci.yml)", "http://doc.gitlab.com/ci/yaml/README.html", target: :blank}. + You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} + %li + Visit #{link_to 'GitLab project settings', @project.gitlab_url + "/services/gitlab_ci/edit", target: :blank} + and press the "Test settings" button. + %li + Return to this page and refresh it, it should show a new build. diff --git a/app/views/ci/shared/_no_runners.html.haml b/app/views/ci/shared/_no_runners.html.haml new file mode 100644 index 00000000000..f56c37d9b37 --- /dev/null +++ b/app/views/ci/shared/_no_runners.html.haml @@ -0,0 +1,7 @@ +.alert.alert-danger + %p + Now you need Runners to process your builds. + %span + Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it + + diff --git a/app/views/ci/user_sessions/new.html.haml b/app/views/ci/user_sessions/new.html.haml new file mode 100644 index 00000000000..308b217ea78 --- /dev/null +++ b/app/views/ci/user_sessions/new.html.haml @@ -0,0 +1,8 @@ +.login-block + %h2 Login using GitLab account + %p.light + Make sure you have account on GitLab server + = link_to GitlabCi.config.gitlab_server.url, GitlabCi.config.gitlab_server.url, no_turbolink + %hr + = link_to "Login with GitLab", auth_ci_user_sessions_path(state: params[:state]), no_turbolink.merge( class: 'btn btn-login btn-success' ) + diff --git a/app/views/ci/web_hooks/index.html.haml b/app/views/ci/web_hooks/index.html.haml new file mode 100644 index 00000000000..78e8203b25e --- /dev/null +++ b/app/views/ci/web_hooks/index.html.haml @@ -0,0 +1,92 @@ +%h3.page-title + Web hooks + +%p.light + Web Hooks can be used for binding events when build completed. + +%hr.clearfix + += form_for [:ci, @project, @web_hook], html: { class: 'form-horizontal' } do |f| + -if @web_hook.errors.any? + .alert.alert-danger + - @web_hook.errors.full_messages.each do |msg| + %p= msg + .form-group + = f.label :url, "URL", class: 'control-label' + .col-sm-10 + = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json' + .form-actions + = f.submit "Add Web Hook", class: "btn btn-create" + +-if @web_hooks.any? + %h4 Activated web hooks (#{@web_hooks.count}) + %table.table + - @web_hooks.each do |hook| + %tr + %td + .clearfix + %span.monospace= hook.url + %td + .pull-right + - if @project.commits.any? + = link_to 'Test Hook', test_ci_project_web_hook_path(@project, hook), class: "btn btn-sm btn-grouped" + = link_to 'Remove', ci_project_web_hook_path(@project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" + +%h4 Web Hook data example + +:erb + <pre> + <code> + { + "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" + } + } + </code> + </pre> diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml index 1db56542afd..19d919f9b6a 100644 --- a/app/views/dashboard/_activities.html.haml +++ b/app/views/dashboard/_activities.html.haml @@ -5,7 +5,7 @@ - if current_user %ul.nav.nav-pills.event_filter.pull-right %li.pull-right - = link_to dashboard_path(:atom, { private_token: current_user.private_token }), class: 'rss-btn' do + = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'rss-btn' do %i.fa.fa-rss = render 'shared/event_filter' diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index dcd6c97d44d..64bd356f546 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -1,7 +1,7 @@ %ul.center-top-menu - = nav_link(page: [dashboard_groups_path]) do + = nav_link(page: dashboard_groups_path) do = link_to dashboard_groups_path, title: 'Your groups', data: {placement: 'right'} do Your Groups - = nav_link(page: [explore_groups_path]) do + = nav_link(page: explore_groups_path) do = link_to explore_groups_path, title: 'Explore groups', data: {placement: 'bottom'} do Explore Groups diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 13a5eae3cdc..ed480b8caf8 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -1,6 +1,6 @@ %ul.center-top-menu - = nav_link(path: ['dashboard#show', 'root#show']) do - = link_to dashboard_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do + = nav_link(path: ['projects#index', 'root#index']) do + = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do Your Projects = nav_link(page: starred_dashboard_projects_path) do = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml new file mode 100644 index 00000000000..0ae62d6f1b6 --- /dev/null +++ b/app/views/dashboard/_snippets_head.html.haml @@ -0,0 +1,7 @@ +%ul.center-top-menu + = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do + = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do + Your Snippets + = nav_link(page: explore_snippets_path) do + = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do + Explore Snippets diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml index 3e24338af64..aa57df14c23 100644 --- a/app/views/dashboard/activity.html.haml +++ b/app/views/dashboard/activity.html.haml @@ -1,8 +1,10 @@ = content_for :meta_tags do - if current_user - = auto_discovery_link_tag(:atom, dashboard_url(format: :atom, private_token: current_user.private_token), title: "All activity") + = auto_discovery_link_tag(:atom, dashboard_projects_url(format: :atom, private_token: current_user.private_token), title: "All activity") +- page_title "Activity" - header_title "Activity", activity_dashboard_path + = render 'dashboard/activity_head' %section.activities diff --git a/app/views/dashboard/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml index ef9b9ce756a..e09e032a7f1 100644 --- a/app/views/dashboard/_projects.html.haml +++ b/app/views/dashboard/projects/_projects.html.haml @@ -4,7 +4,7 @@ = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' - if current_user.can_create_project? %span.input-group-btn - = link_to new_project_path, class: 'btn btn-success' do + = link_to new_project_path, class: 'btn btn-green' do New project - = render 'shared/projects/list', projects: @projects + = render 'shared/projects/list', projects: @projects, ci: true diff --git a/app/views/dashboard/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml index 4e7d6639727..4e7d6639727 100644 --- a/app/views/dashboard/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml diff --git a/app/views/dashboard/show.atom.builder b/app/views/dashboard/projects/index.atom.builder index e9a612231d5..d2c51486841 100644 --- a/app/views/dashboard/show.atom.builder +++ b/app/views/dashboard/projects/index.atom.builder @@ -1,9 +1,9 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "Activity" - xml.link href: dashboard_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" - xml.link href: dashboard_url, rel: "alternate", type: "text/html" - xml.id dashboard_url + xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html" + xml.id dashboard_projects_url xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| diff --git a/app/views/dashboard/show.html.haml b/app/views/dashboard/projects/index.html.haml index 1d5324e0d72..7a16b811f6b 100644 --- a/app/views/dashboard/show.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -1,8 +1,10 @@ = content_for :meta_tags do - if current_user - = auto_discovery_link_tag(:atom, dashboard_url(format: :atom, private_token: current_user.private_token), title: "All activity") + = auto_discovery_link_tag(:atom, dashboard_projects_url(format: :atom, private_token: current_user.private_token), title: "All activity") + +- page_title "Projects" +- header_title "Projects", root_path -- header_title "Projects", (current_user ? root_path : explore_root_path) = render 'dashboard/projects_head' - if @last_push diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml index 2fd7a1cf16c..339362701d4 100644 --- a/app/views/dashboard/projects/starred.html.haml +++ b/app/views/dashboard/projects/starred.html.haml @@ -1,9 +1,10 @@ - page_title "Starred Projects" -- header_title "Projects", (current_user ? root_path : explore_root_path) +- header_title "Projects", projects_path + = render 'dashboard/projects_head' - if @projects.any? - = render 'dashboard/projects' + = render 'projects' - else %h3 You don't have starred projects yet %p.slead Visit project page and press on star icon and it will appear on this page. diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml new file mode 100644 index 00000000000..d3908062f43 --- /dev/null +++ b/app/views/dashboard/snippets/index.html.haml @@ -0,0 +1,38 @@ +- page_title "Snippets" +- header_title "Snippets", dashboard_snippets_path + += render 'dashboard/snippets_head' + +.gray-content-block + .pull-right + = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do + Add new snippet + + .oneline + Share code pastes with others out of git repository + +%ul.nav.nav-tabs.prepend-top-20 + = nav_tab :scope, nil do + = link_to dashboard_snippets_path do + All + %span.badge + = current_user.snippets.count + = nav_tab :scope, 'are_private' do + = link_to dashboard_snippets_path(scope: 'are_private') do + Private + %span.badge + = current_user.snippets.are_private.count + = nav_tab :scope, 'are_internal' do + = link_to dashboard_snippets_path(scope: 'are_internal') do + Internal + %span.badge + = current_user.snippets.are_internal.count + = nav_tab :scope, 'are_public' do + = link_to dashboard_snippets_path(scope: 'are_public') do + Public + %span.badge + = current_user.snippets.are_public.count + +.my-snippets + = render 'snippets/snippets' + diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml index 6a0c6cba41b..ffc37ad6178 100644 --- a/app/views/events/_event_last_push.html.haml +++ b/app/views/events/_event_last_push.html.haml @@ -1,14 +1,14 @@ - if show_last_push_widget?(event) - .event-last-push - .event-last-push-text - %span You pushed to - = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do - %strong= event.ref_name - %span at - %strong= link_to_project event.project - #{time_ago_with_tooltip(event.created_at)} + .gray-content-block.clear-block + .event-last-push + .event-last-push-text + %span You pushed to + = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do + %strong= event.ref_name + %span at + %strong= link_to_project event.project + #{time_ago_with_tooltip(event.created_at)} - .pull-right - = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do - Create Merge Request - %hr + .pull-right + = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do + Create Merge Request diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index a39e62e9dac..4ecf1c33d2a 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -5,13 +5,14 @@ - if event.target %strong= link_to "##{event.target_iid}", [event.project.namespace.becomes(Namespace), event.project, event.target] - at + + = event_preposition(event) - if event.project = link_to_project event.project - else = event.project_name - + - if event.target.respond_to?(:title) .event-body .event-note diff --git a/app/views/explore/_head.html.haml b/app/views/explore/_head.html.haml new file mode 100644 index 00000000000..d8a57560788 --- /dev/null +++ b/app/views/explore/_head.html.haml @@ -0,0 +1,6 @@ +.explore-title + %h3 + Explore GitLab + %p.lead + Discover projects, groups and snippets. Share your projects with others + %br diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index e8a6752de8c..83d4d321c83 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -1,7 +1,11 @@ -- page_title "Groups" -- header_title "Groups", (current_user ? dashboard_groups_path : explore_groups_path) +- page_title "Groups" +- header_title "Groups", dashboard_groups_path + - if current_user = render 'dashboard/groups_head' +- else + = render 'explore/head' + .gray-content-block.clearfix .pull-left = form_tag explore_groups_path, method: :get, class: 'form-inline form-tiny' do |f| diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml index 9df5b3830a8..67e38ca3127 100644 --- a/app/views/explore/projects/index.html.haml +++ b/app/views/explore/projects/index.html.haml @@ -1,6 +1,11 @@ -- page_title "Projects" +- page_title "Projects" +- header_title "Projects", root_path + - if current_user = render 'dashboard/projects_head' +- else + = render 'explore/head' + .gray-content-block.clearfix = render 'filter' = render 'projects', projects: @projects diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml index a9df32f3d7d..596cb0a96cd 100644 --- a/app/views/explore/projects/starred.html.haml +++ b/app/views/explore/projects/starred.html.haml @@ -1,6 +1,10 @@ -- page_title "Starred Projects" +- page_title "Projects" +- header_title "Projects", root_path + - if current_user = render 'dashboard/projects_head' +- else + = render 'explore/head' .explore-trending-block .gray-content-block diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml index c1ef06f6cdb..5ea6d81c5b9 100644 --- a/app/views/explore/projects/trending.html.haml +++ b/app/views/explore/projects/trending.html.haml @@ -1,13 +1,11 @@ -- page_title "Trending Projects" +- page_title "Projects" +- header_title "Projects", root_path + - if current_user = render 'dashboard/projects_head' - else - .explore-title - %h3 - Explore GitLab - %p.lead - Discover projects and groups. Share your projects with others - %br + = render 'explore/head' + .explore-trending-block .gray-content-block .pull-right diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml new file mode 100644 index 00000000000..7e4fa7d4873 --- /dev/null +++ b/app/views/explore/snippets/index.html.haml @@ -0,0 +1,18 @@ +- page_title "Snippets" +- header_title "Snippets", snippets_path + +- if current_user + = render 'dashboard/snippets_head' +- else + = render 'explore/head' + +.gray-content-block + - if current_user + .pull-right + = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do + Add new snippet + + .oneline + Public snippets created by you and other users are listed here + += render 'snippets/snippets' diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 9ac56b1e5fe..2b27a88794d 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -4,7 +4,7 @@ = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' - if can? current_user, :create_projects, @group %span.input-group-btn - = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-success' do + = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-green' do New project = render 'shared/projects/list', projects: @projects, projects_limit: 20, stars: false diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 2ff4b7e23ea..ae8fc9f85f0 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -1,3 +1,6 @@ +- header_title group_title(@group, "Settings", edit_group_path(@group)) +- @blank_container = true + .panel.panel-default .panel-heading %strong= @group.name diff --git a/app/views/groups/milestones/_header_title.html.haml b/app/views/groups/milestones/_header_title.html.haml new file mode 100644 index 00000000000..d7fabf53587 --- /dev/null +++ b/app/views/groups/milestones/_header_title.html.haml @@ -0,0 +1 @@ +- header_title group_title(@group, "Milestones", group_milestones_path(@group)) diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index 8f2decb851f..0c213f42186 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -1,4 +1,6 @@ - page_title @group_milestone.title, "Milestones" += render "header_title" + %h4.page-title .issue-box{ class: "issue-box-#{@group_milestone.closed? ? 'closed' : 'open'}" } - if @group_milestone.closed? diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index d06cfa7ff9f..f1d507a50c7 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -1,4 +1,6 @@ - page_title "Projects" +- header_title group_title(@group, "Projects", projects_group_path(@group)) + .panel.panel-default .panel-heading %strong= @group.name diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 0577f4ec142..a9ba9d2ba10 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,3 +1,6 @@ +- unless can?(current_user, :read_group, @group) + - @disable_search_panel = true + = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") @@ -16,22 +19,25 @@ = render 'shared/show_aside' - .row - %section.activities.col-md-7 - .hidden-xs - - if current_user - = render "events/event_last_push", event: @last_push - + - if can?(current_user, :read_group, @group) + .row + %section.activities.col-md-7 + .hidden-xs - if current_user + = render "events/event_last_push", event: @last_push + %ul.nav.nav-pills.event_filter.pull-right %li = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'rss-btn' do %i.fa.fa-rss - = render 'shared/event_filter' - %hr + = render 'shared/event_filter' + %hr - .content_list - = spinner - %aside.side.col-md-5 - = render "projects", projects: @projects + .content_list + = spinner + %aside.side.col-md-5 + = render "projects", projects: @projects + - else + %p + This group does not have public projects diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index bf4b7234b21..57bc91ea5a9 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -1,14 +1,15 @@ %div %h1 GitLab - %span= Gitlab::VERSION - %small= Gitlab::REVISION - - if current_application_settings.version_check_enabled + Community Edition + - if user_signed_in? + %span= Gitlab::VERSION + %small= Gitlab::REVISION = version_status_badge %p.slead GitLab is open source software to collaborate on code. %br - Manage git repositories with fine grained access controls that keep your code secure. + Manage git repositories with fine-grained access controls that keep your code secure. %br Perform code reviews and enhance collaboration with merge requests. %br @@ -17,6 +18,9 @@ Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises. %br Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}. + - if current_application_settings.help_page_text.present? + %hr + = markdown(current_application_settings.help_page_text) %hr @@ -24,29 +28,14 @@ .col-md-8 .documentation-index = preserve do - - readme_text = File.read(Rails.root.join("doc", "README.md")) - - text = readme_text.dup - - readme_text.scan(/\]\(([^(]+)\)/) { |match| text.gsub!(match.first, "help/#{match.first}") } - = markdown text - + = markdown(@help_index) .col-md-4 .panel.panel-default .panel-heading Quick help %ul.well-list - %li - See our website for - = link_to 'getting help', promo_url + '/getting-help/' - %li - Use the - = link_to 'search bar', '#', onclick: 'Shortcuts.focusSearch(event)' - on the top of this page - %li - Use - = link_to 'shortcuts', '#', onclick: 'Shortcuts.showHelp(event)' - %li - Get a support - = link_to 'subscription', 'https://about.gitlab.com/pricing/' - %li - = link_to 'Compare', 'https://about.gitlab.com/features/#compare' - GitLab editions + %li= link_to 'See our website for getting help', promo_url + '/getting-help/' + %li= link_to 'Use the search bar on the top of this page', '#', onclick: 'Shortcuts.focusSearch(event)' + %li= link_to 'Use shortcuts', '#', onclick: 'Shortcuts.showHelp(event)' + %li= link_to 'Get a support subscription', 'https://about.gitlab.com/pricing/' + %li= link_to 'Compare GitLab editions', 'https://about.gitlab.com/features/#compare' diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml index 8551496b98a..0398afb4c1d 100644 --- a/app/views/help/show.html.haml +++ b/app/views/help/show.html.haml @@ -1,3 +1,3 @@ - page_title @file.humanize, *@category.split("/").reverse.map(&:humanize) .documentation.wiki - = markdown @markdown.gsub('$your_email', current_user.email) + = markdown @markdown.gsub('$your_email', current_user.try(:email) || "email@example.com") diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml new file mode 100644 index 00000000000..e1bb88ca4ed --- /dev/null +++ b/app/views/import/fogbugz/new.html.haml @@ -0,0 +1,25 @@ +- page_title "FogBugz Import" +%h3.page-title + %i.fa.fa-bug + Import projects from FogBugz +%hr + += form_tag callback_import_fogbugz_path, class: 'form-horizontal' do + %p + To get started you enter your FogBugz URL and login information below. + In the next steps, you'll be able to map users and select the projects + you want to import. + .form-group + = label_tag :uri, 'FogBugz URL', class: 'control-label' + .col-sm-4 + = text_field_tag :uri, nil, placeholder: 'https://mycompany.fogbugz.com', class: 'form-control' + .form-group + = label_tag :email, 'FogBugz Email', class: 'control-label' + .col-sm-4 + = text_field_tag :email, nil, class: 'form-control' + .form-group + = label_tag :password, 'FogBugz Password', class: 'control-label' + .col-sm-4 + = password_field_tag :password, nil, class: 'form-control' + .form-actions + = submit_tag 'Continue to the next step', class: 'btn btn-create' diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml new file mode 100644 index 00000000000..25cebfb3665 --- /dev/null +++ b/app/views/import/fogbugz/new_user_map.html.haml @@ -0,0 +1,49 @@ +- page_title 'User map', 'FogBugz import' +%h3.page-title + %i.fa.fa-bug + Import projects from FogBugz +%hr + += form_tag create_user_map_import_fogbugz_path, class: 'form-horizontal' do + %p + Customize how FogBugz email addresses and usernames are imported into GitLab. + In the next step, you'll be able to select the projects you want to import. + %p + The user map is a mapping of the FogBugz users that participated on your projects to the way their email address and usernames wil be imported into GitLab. You can change this by populating the table below. + %ul + %li + %strong Default: Map a FogBugz account ID to a full name + %p + An empty GitLab User field will add the FogBugz user's full name + (e.g. "By John Smith") in the description of all issues and comments. + It will also associate and/or assign these issues and comments with + the project creator. + %li + %strong Map a FogBugz account ID to a GitLab user + %p + Selecting a GitLab user will add a link to the GitLab user in the descriptions + of issues and comments (e.g. "By <a href="#">@johnsmith</a>"). It will also + associate and/or assign these issues and comments with the selected user. + + %table.table + %thead + %tr + %th ID + %th Name + %th Email + %th GitLab User + %tbody + - @user_map.each do |id, user| + %tr + %td= id + %td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control' + %td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control' + %td + = users_select_tag("users[#{id}][gitlab_user]", class: 'custom-form-control', + scope: :all, email_user: true, selected: user[:gitlab_user]) + + .form-actions + = submit_tag 'Continue to the next step', class: 'btn btn-create' + +:coffeescript + new UsersSelect() diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml new file mode 100644 index 00000000000..f179ece402d --- /dev/null +++ b/app/views/import/fogbugz/status.html.haml @@ -0,0 +1,51 @@ +- page_title "FogBugz import" +%h3.page-title + %i.fa.fa-bug + Import projects from FogBugz + +- if @repos.any? + %p.light + Select projects you want to import. + %p.light + Optionally, you can + = link_to 'customize', new_user_map_import_fogbugz_path + how FogBugz email addresses and usernames are imported into GitLab. + %hr + %p + = button_tag 'Import all projects', class: 'btn btn-success js-import-all' + +%table.table.import-jobs + %thead + %tr + %th From FogBugz + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = project.import_source + %td + %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name + + - @repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td + = repo.name + %td.import-target + = "#{current_user.username}/#{repo.name}" + %td.import-actions.job-status + = button_tag "Import", class: "btn js-add-to-import" + +:coffeescript + new ImporterStatus("#{jobs_import_fogbugz_path}", "#{import_fogbugz_path}") diff --git a/app/views/kaminari/gitlab/_first_page.html.haml b/app/views/kaminari/gitlab/_first_page.html.haml index 41c9c0b3af6..ada7306d98d 100644 --- a/app/views/kaminari/gitlab/_first_page.html.haml +++ b/app/views/kaminari/gitlab/_first_page.html.haml @@ -5,5 +5,5 @@ -# num_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%span.first +%li.first = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, remote: remote diff --git a/app/views/kaminari/gitlab/_last_page.html.haml b/app/views/kaminari/gitlab/_last_page.html.haml index b03a206224c..3431d029bcc 100644 --- a/app/views/kaminari/gitlab/_last_page.html.haml +++ b/app/views/kaminari/gitlab/_last_page.html.haml @@ -5,5 +5,5 @@ -# num_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%span.last +%li.last = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {remote: remote} diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml index 4f7996e4996..2f645186921 100644 --- a/app/views/kaminari/gitlab/_paginator.html.haml +++ b/app/views/kaminari/gitlab/_paginator.html.haml @@ -7,11 +7,16 @@ -# paginator: the paginator that renders the pagination tags inside = paginator.render do %div.gl-pagination - %ul.pagination - = prev_page_tag unless current_page.first? + %ul.pagination.clearfix + - unless current_page.first? + = first_page_tag unless num_pages < 5 # As kaminari will always show the first 5 pages + = prev_page_tag - each_page do |page| - if page.left_outer? || page.right_outer? || page.inside_window? = page_tag page - elsif !page.was_truncated? = gap_tag - = next_page_tag unless current_page.last? + - unless current_page.last? + = next_page_tag + = last_page_tag unless num_pages < 5 + diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 397649dacf8..c3b137e3ddf 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -3,6 +3,7 @@ %meta{charset: "utf-8"} %meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'} %meta{content: "GitLab Community Edition", name: "description"} + %meta{name: 'referrer', content: 'origin'} %title= page_title diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index c1746676ae2..2468687b56d 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -6,10 +6,14 @@ = brand_header_logo .gitlab-text-container %h3 GitLab + - if defined?(sidebar) && sidebar = render "layouts/nav/#{sidebar}" - elsif current_user = render 'layouts/nav/dashboard' + - else + = render 'layouts/nav/explore' + .collapse-nav = render partial: 'layouts/collapse_button' - if current_user @@ -19,7 +23,7 @@ = current_user.username .content-wrapper = render "layouts/flash" - %div{ class: fluid_layout ? "container-fluid" : "container-fluid container-limited" } + %div{ class: container_class } .content .clearfix = yield diff --git a/app/views/layouts/ci/_info.html.haml b/app/views/layouts/ci/_info.html.haml new file mode 100644 index 00000000000..24c68a6dbf5 --- /dev/null +++ b/app/views/layouts/ci/_info.html.haml @@ -0,0 +1,2 @@ +- if current_user && current_user.is_admin? && Ci::Runner.count.zero? + = render 'ci/shared/no_runners' diff --git a/app/views/layouts/ci/_nav_admin.html.haml b/app/views/layouts/ci/_nav_admin.html.haml new file mode 100644 index 00000000000..af2545a22d8 --- /dev/null +++ b/app/views/layouts/ci/_nav_admin.html.haml @@ -0,0 +1,33 @@ +%ul.nav.nav-sidebar + = nav_link do + = link_to admin_root_path, title: 'Back to admin', data: {placement: 'right'}, class: 'back-link' do + = icon('caret-square-o-left fw') + %span + Back to admin + + %li.separate-item + = nav_link path: 'projects#index' do + = link_to ci_admin_projects_path do + = icon('list-alt fw') + Projects + = nav_link path: 'events#index' do + = link_to ci_admin_events_path do + = icon('book fw') + Events + = nav_link path: ['runners#index', 'runners#show'] do + = link_to ci_admin_runners_path do + = icon('cog fw') + Runners + %small.pull-right + = Ci::Runner.count(:all) + = nav_link path: 'builds#index' do + = link_to ci_admin_builds_path do + = icon('link fw') + Builds + %small.pull-right + = Ci::Build.count(:all) + = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do + = link_to ci_admin_application_settings_path do + = icon('cogs fw') + %span + Settings diff --git a/app/views/layouts/ci/_nav_project.html.haml b/app/views/layouts/ci/_nav_project.html.haml new file mode 100644 index 00000000000..3a2741367c1 --- /dev/null +++ b/app/views/layouts/ci/_nav_project.html.haml @@ -0,0 +1,28 @@ +%ul.nav.nav-sidebar + = nav_link do + = link_to project_path(@project.gl_project), title: 'Back to project', data: {placement: 'right'}, class: 'back-link' do + = icon('caret-square-o-left fw') + %span + Back to project + %li.separate-item + = nav_link path: ['projects#show', 'commits#show', 'builds#show'] do + = link_to ci_project_path(@project) do + = icon('list-alt fw') + %span + Commits + %span.count= @project.commits.count + = nav_link path: 'web_hooks#index' do + = link_to ci_project_web_hooks_path(@project) do + = icon('link fw') + %span + Web Hooks + = nav_link path: ['services#index', 'services#edit'] do + = link_to ci_project_services_path(@project) do + = icon('share fw') + %span + Services + = nav_link path: 'events#index' do + = link_to ci_project_events_path(@project) do + = icon('book fw') + %span + Events diff --git a/app/views/layouts/ci/_page.html.haml b/app/views/layouts/ci/_page.html.haml new file mode 100644 index 00000000000..bb5ec727bff --- /dev/null +++ b/app/views/layouts/ci/_page.html.haml @@ -0,0 +1,27 @@ +.page-with-sidebar{ class: nav_sidebar_class } + = render "layouts/broadcast" + .sidebar-wrapper.nicescroll + .header-logo + = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do + = brand_header_logo + .gitlab-text-container + %h3 GitLab + + - if defined?(sidebar) && sidebar + = render "layouts/ci/#{sidebar}" + - elsif current_user + = render 'layouts/nav/dashboard' + .collapse-nav + = render partial: 'layouts/collapse_button' + - if current_user + = link_to current_user, class: 'sidebar-user' do + = image_tag avatar_icon(current_user.email, 60), alt: 'User activity', class: 'avatar avatar s36' + .username + = current_user.username + .content-wrapper + = render "layouts/flash" + = render 'layouts/ci/info' + %div{ class: container_class } + .content + .clearfix + = yield diff --git a/app/views/layouts/ci/admin.html.haml b/app/views/layouts/ci/admin.html.haml new file mode 100644 index 00000000000..c8cb185d28c --- /dev/null +++ b/app/views/layouts/ci/admin.html.haml @@ -0,0 +1,11 @@ +!!! 5 +%html{ lang: "en"} + = render 'layouts/head' + %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page} + - header_title = "Admin area" + - if current_user + = render "layouts/header/default", title: header_title + - else + = render "layouts/header/public", title: header_title + + = render 'layouts/ci/page', sidebar: 'nav_admin' diff --git a/app/views/layouts/ci/application.html.haml b/app/views/layouts/ci/application.html.haml new file mode 100644 index 00000000000..38023468d0b --- /dev/null +++ b/app/views/layouts/ci/application.html.haml @@ -0,0 +1,11 @@ +!!! 5 +%html{ lang: "en"} + = render 'layouts/head' + %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page} + - header_title = "Continuous Integration" + - if current_user + = render "layouts/header/default", title: header_title + - else + = render "layouts/header/public", title: header_title + + = render 'layouts/ci/page' diff --git a/app/views/layouts/ci/build.html.haml b/app/views/layouts/ci/build.html.haml new file mode 100644 index 00000000000..a1356f0dc2e --- /dev/null +++ b/app/views/layouts/ci/build.html.haml @@ -0,0 +1,11 @@ +!!! 5 +%html{ lang: "en"} + = render 'layouts/head' + %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page} + - header_title ci_commit_title(@commit) + - if current_user + = render "layouts/header/default", title: header_title + - else + = render "layouts/header/public", title: header_title + + = render 'layouts/ci/page', sidebar: 'nav_project' diff --git a/app/views/layouts/ci/commit.html.haml b/app/views/layouts/ci/commit.html.haml new file mode 100644 index 00000000000..a1356f0dc2e --- /dev/null +++ b/app/views/layouts/ci/commit.html.haml @@ -0,0 +1,11 @@ +!!! 5 +%html{ lang: "en"} + = render 'layouts/head' + %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page} + - header_title ci_commit_title(@commit) + - if current_user + = render "layouts/header/default", title: header_title + - else + = render "layouts/header/public", title: header_title + + = render 'layouts/ci/page', sidebar: 'nav_project' diff --git a/app/views/layouts/ci/notify.html.haml b/app/views/layouts/ci/notify.html.haml new file mode 100644 index 00000000000..270b206df5e --- /dev/null +++ b/app/views/layouts/ci/notify.html.haml @@ -0,0 +1,19 @@ +%html{lang: "en"} + %head + %meta{content: "text/html; charset=utf-8", "http-equiv" => "Content-Type"} + %title + GitLab CI + + %body + = yield :header + + %table{align: "left", border: "0", cellpadding: "0", cellspacing: "0", style: "padding: 10px 0;", width: "100%"} + %tr + %td{align: "left", style: "margin: 0; padding: 10px;"} + = yield + %br + %tr + %td{align: "left", style: "margin: 0; padding: 10px;"} + %p{style: "font-size:small;color:#777"} + - if @project + You're receiving this notification because you are the one who triggered a build on the #{@project.name} project. diff --git a/app/views/layouts/ci/project.html.haml b/app/views/layouts/ci/project.html.haml new file mode 100644 index 00000000000..15478c3f5bc --- /dev/null +++ b/app/views/layouts/ci/project.html.haml @@ -0,0 +1,11 @@ +!!! 5 +%html{ lang: "en"} + = render 'layouts/head' + %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page} + - header_title @project.name, ci_project_path(@project) + - if current_user + = render "layouts/header/default", title: header_title + - else + = render "layouts/header/public", title: header_title + + = render 'layouts/ci/page', sidebar: 'nav_project' diff --git a/app/views/layouts/dashboard.html.haml b/app/views/layouts/dashboard.html.haml index fad7de69432..cb96bcc2cf4 100644 --- a/app/views/layouts/dashboard.html.haml +++ b/app/views/layouts/dashboard.html.haml @@ -1,6 +1,5 @@ - page_title "Dashboard" -- unless @header_title - - header_title "Dashboard", root_path +- header_title "Dashboard", root_path unless header_title - sidebar "dashboard" = render template: "layouts/application" diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 1987bf1592a..95e077c339f 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -31,5 +31,5 @@ .container .footer-links = link_to "Explore", explore_root_path - = link_to "Documentation", "http://doc.gitlab.com/" + = link_to "Help", help_path = link_to "About GitLab", "https://about.gitlab.com/" diff --git a/app/views/layouts/explore.html.haml b/app/views/layouts/explore.html.haml index 9098554e6f0..df65792be73 100644 --- a/app/views/layouts/explore.html.haml +++ b/app/views/layouts/explore.html.haml @@ -1,9 +1,5 @@ - page_title "Explore" -- if current_user - - unless @header_title - - header_title "Projects", (current_user ? root_path : explore_root_path) -- else +- unless current_user - header_title "Explore GitLab", explore_root_path -- sidebar "dashboard" = render template: "layouts/application" diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index 4f00d01d4cd..31888c5580e 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -1,6 +1,5 @@ - page_title @group.name -- unless @header_title - - header_title @group.name, group_path(@group) -- sidebar "group" unless sidebar +- header_title group_title(@group) unless header_title +- sidebar "group" unless sidebar = render template: "layouts/application" diff --git a/app/views/layouts/group_settings.html.haml b/app/views/layouts/group_settings.html.haml index e303a561628..a1a1fc2f858 100644 --- a/app/views/layouts/group_settings.html.haml +++ b/app/views/layouts/group_settings.html.haml @@ -1,4 +1,5 @@ - page_title "Settings" +- header_title group_title(@group, "Settings", edit_group_path(@group)) - sidebar "group_settings" = render template: "layouts/group" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 3892f71c0e3..c31b1cbe9a8 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -7,8 +7,9 @@ .navbar-collapse.collapse %ul.nav.navbar-nav.pull-right - %li.hidden-sm.hidden-xs - = render 'layouts/search' + - unless @disable_search_panel + %li.hidden-sm.hidden-xs + = render 'layouts/search' %li.visible-sm.visible-xs = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom'} do = icon('search') diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 2065be3828a..2079feeeab6 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -4,7 +4,7 @@ = icon('dashboard fw') %span Overview - = nav_link(controller: :projects) do + = nav_link(controller: [:admin, :projects]) do = link_to admin_namespaces_projects_path, title: 'Projects', data: {placement: 'right'} do = icon('cube fw') %span @@ -24,6 +24,11 @@ = icon('key fw') %span Deploy Keys + = nav_link do + = link_to ci_admin_projects_path, title: 'Continuous Integration', data: {placement: 'right'} do + = icon('building fw') + %span + Continuous Integration = nav_link(controller: :logs) do = link_to admin_logs_path, title: 'Logs', data: {placement: 'right'} do = icon('file-text fw') @@ -57,6 +62,12 @@ %span Service Templates + = nav_link(controller: :labels) do + = link_to admin_labels_path, title: 'Labels', data: {placement: 'right'} do + = icon('tags fw') + %span + Labels + = nav_link(controller: :abuse_reports) do = link_to admin_abuse_reports_path, title: "Abuse reports" do = icon('exclamation-circle fw') diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 0cf1c3d5d27..b1a1d531846 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,31 +1,30 @@ %ul.nav.nav-sidebar - = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do - = link_to (current_user ? root_path : explore_root_path), title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do + = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: 'home'}) do + = link_to dashboard_projects_path, title: 'Projects', data: {placement: 'right'} do = icon('home fw') %span Projects = nav_link(path: 'dashboard#activity') do - = link_to activity_dashboard_path, title: 'Activity', data: {placement: 'right'} do + = link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity', data: {placement: 'right'} do = icon('dashboard fw') %span Activity = nav_link(controller: :groups) do - = link_to (current_user ? dashboard_groups_path : explore_groups_path), title: 'Groups', data: {placement: 'right'} do + = link_to dashboard_groups_path, title: 'Groups', data: {placement: 'right'} do = icon('group fw') %span Groups - - if current_user - = nav_link(controller: :milestones) do - = link_to dashboard_milestones_path, title: 'Milestones', data: {placement: 'right'} do - = icon('clock-o fw') - %span - Milestones - = nav_link(path: 'dashboard#issues') do - = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do - = icon('exclamation-circle fw') - %span - Issues - %span.count= current_user.assigned_issues.opened.count + = nav_link(controller: :milestones) do + = link_to dashboard_milestones_path, title: 'Milestones', data: {placement: 'right'} do + = icon('clock-o fw') + %span + Milestones + = nav_link(path: 'dashboard#issues') do + = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do + = icon('exclamation-circle fw') + %span + Issues + %span.count= current_user.assigned_issues.opened.count = nav_link(path: 'dashboard#merge_requests') do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do = icon('tasks fw') @@ -33,18 +32,19 @@ Merge Requests %span.count= current_user.assigned_merge_requests.opened.count = nav_link(controller: :snippets) do - = link_to (current_user ? user_snippets_path(current_user) : snippets_path), title: 'Your snippets', data: {placement: 'right'} do + = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do = icon('clipboard fw') %span Snippets - - if current_user - = nav_link(controller: :profile) do - = link_to profile_path, title: 'Profile settings', data: {toggle: 'tooltip', placement: 'bottom'} do - = icon('user fw') - %span - Profile Settings = nav_link(controller: :help) do = link_to help_path, title: 'Help', data: {placement: 'right'} do = icon('question-circle fw') %span Help + + %li.separate-item + = nav_link(controller: :profile) do + = link_to profile_path, title: 'Profile settings', data: {placement: 'bottom'} do + = icon('user fw') + %span + Profile Settings diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml new file mode 100644 index 00000000000..21e565972a7 --- /dev/null +++ b/app/views/layouts/nav/_explore.html.haml @@ -0,0 +1,21 @@ +%ul.nav.nav-sidebar + = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do + = link_to explore_root_path, title: 'Projects', data: {placement: 'right'} do + = icon('home fw') + %span + Projects + = nav_link(controller: :groups) do + = link_to explore_groups_path, title: 'Groups', data: {placement: 'right'} do + = icon('group fw') + %span + Groups + = nav_link(controller: :snippets) do + = link_to explore_snippets_path, title: 'Snippets', data: {placement: 'right'} do + = icon('clipboard fw') + %span + Snippets + = nav_link(controller: :help) do + = link_to help_path, title: 'Help', data: {placement: 'right'} do + = icon('question-circle fw') + %span + Help diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 695ce68a201..eb35af22b93 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -3,7 +3,7 @@ = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do = icon('caret-square-o-left fw') %span - Back to Dashboard + Back to dashboard %li.separate-item @@ -12,34 +12,35 @@ = icon('dashboard fw') %span Group - - if current_user - = nav_link(controller: [:group, :milestones]) do - = link_to group_milestones_path(@group), title: 'Milestones', data: {placement: 'right'} do - = icon('clock-o fw') + - if can?(current_user, :read_group, @group) + - if current_user + = nav_link(controller: [:group, :milestones]) do + = link_to group_milestones_path(@group), title: 'Milestones', data: {placement: 'right'} do + = icon('clock-o fw') + %span + Milestones + = nav_link(path: 'groups#issues') do + = link_to issues_group_path(@group), title: 'Issues', data: {placement: 'right'} do + = icon('exclamation-circle fw') %span - Milestones - = nav_link(path: 'groups#issues') do - = link_to issues_group_path(@group), title: 'Issues', data: {placement: 'right'} do - = icon('exclamation-circle fw') - %span - Issues - - if current_user - %span.count= Issue.opened.of_group(@group).count - = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group), title: 'Merge Requests', data: {placement: 'right'} do - = icon('tasks fw') - %span - Merge Requests - - if current_user - %span.count= MergeRequest.opened.of_group(@group).count - = nav_link(controller: [:group_members]) do - = link_to group_group_members_path(@group), title: 'Members', data: {placement: 'right'} do - = icon('users fw') - %span - Members - - if can?(current_user, :admin_group, @group) - = nav_link(html_options: { class: "separate-item" }) do - = link_to edit_group_path(@group), title: 'Settings', data: {placement: 'right'} do - = icon ('cogs fw') + Issues + - if current_user + %span.count= Issue.opened.of_group(@group).count + = nav_link(path: 'groups#merge_requests') do + = link_to merge_requests_group_path(@group), title: 'Merge Requests', data: {placement: 'right'} do + = icon('tasks fw') + %span + Merge Requests + - if current_user + %span.count= MergeRequest.opened.of_group(@group).count + = nav_link(controller: [:group_members]) do + = link_to group_group_members_path(@group), title: 'Members', data: {placement: 'right'} do + = icon('users fw') %span - Settings + Members + - if can?(current_user, :admin_group, @group) + = nav_link(html_options: { class: "separate-item" }) do + = link_to edit_group_path(@group), title: 'Settings', data: {placement: 'right'} do + = icon ('cogs fw') + %span + Settings diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 33fd5fcef6c..5a47b8e6db2 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -3,7 +3,7 @@ = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do = icon('caret-square-o-left fw') %span - Back to Dashboard + Back to dashboard %li.separate-item @@ -11,7 +11,7 @@ = link_to profile_path, title: 'Profile', data: {placement: 'right'} do = icon('user fw') %span - Profile + Profile Settings = nav_link(controller: [:accounts, :two_factor_auths]) do = link_to profile_account_path, title: 'Account', data: {placement: 'right'} do = icon('gear fw') diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 5e7b902622b..a218ec7486c 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -4,13 +4,13 @@ = link_to group_path(@project.group), title: 'Back to group', data: {placement: 'right'}, class: 'back-link' do = icon('caret-square-o-left fw') %span - Back to Group + Back to group - else = nav_link do = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do = icon('caret-square-o-left fw') %span - Back to Dashboard + Back to dashboard %li.separate-item @@ -26,28 +26,28 @@ Activity - if project_nav_tab? :files = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do - = link_to namespace_project_tree_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Files', class: 'shortcuts-tree', data: {placement: 'right'} do + = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree', data: {placement: 'right'} do = icon('files-o fw') %span Files - if project_nav_tab? :commits = nav_link(controller: %w(commit commits compare repositories tags branches)) do - = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Commits', class: 'shortcuts-commits', data: {placement: 'right'} do + = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits', data: {placement: 'right'} do = icon('history fw') %span Commits - if project_nav_tab? :network = nav_link(controller: %w(network)) do - = link_to namespace_project_network_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Network', class: 'shortcuts-network', data: {placement: 'right'} do + = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network', data: {placement: 'right'} do = icon('code-fork fw') %span Network - if project_nav_tab? :graphs = nav_link(controller: %w(graphs)) do - = link_to namespace_project_graph_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Graphs', class: 'shortcuts-graphs', data: {placement: 'right'} do + = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs', data: {placement: 'right'} do = icon('area-chart fw') %span Graphs @@ -76,6 +76,13 @@ Merge Requests %span.count.merge_counter= @project.merge_requests.opened.count + - if @project.gitlab_ci? + = nav_link(controller: [:ci, :project]) do + = link_to ci_project_path(@project.gitlab_ci_project), title: 'Continuous Integration', data: {placement: 'right'} do + = icon('building fw') + %span + Continuous Integration + - if project_nav_tab? :settings = nav_link(controller: [:project_members, :teams]) do = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab', data: {placement: 'right'} do diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 857fb199957..26cccb48f68 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -34,3 +34,24 @@ %span Protected Branches + - if @project.gitlab_ci? + = nav_link(controller: :runners) do + = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners', data: {placement: 'right'} do + = icon('cog fw') + %span + Runners + = nav_link(controller: :variables) do + = link_to namespace_project_variables_path(@project.namespace, @project) do + = icon('code fw') + %span + Variables + = nav_link path: 'triggers#index' do + = link_to namespace_project_triggers_path(@project.namespace, @project) do + = icon('retweet fw') + %span + Triggers + = nav_link path: 'ci_settings#edit' do + = link_to edit_namespace_project_ci_settings_path(@project.namespace, @project) do + = icon('building fw') + %span + CI Settings diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index 77d2ccbf762..dfa6cc5702e 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -1,5 +1,5 @@ - page_title "Profile Settings" -- header_title "Profile Settings", profile_path +- header_title "Profile Settings", profile_path unless header_title - sidebar "profile" = render template: "layouts/application" diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 44afa33dfe5..78dafcd8bfa 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -1,6 +1,6 @@ - page_title @project.name_with_namespace -- header_title project_title(@project) -- sidebar "project" unless sidebar +- header_title project_title(@project) unless header_title +- sidebar "project" unless sidebar - content_for :scripts_body_top do - if current_user diff --git a/app/views/layouts/project_settings.html.haml b/app/views/layouts/project_settings.html.haml index 43401668334..59ce38f67bb 100644 --- a/app/views/layouts/project_settings.html.haml +++ b/app/views/layouts/project_settings.html.haml @@ -1,4 +1,5 @@ - page_title "Settings" +- header_title project_title(@project, "Settings", edit_project_path(@project)) - sidebar "project_settings" = render template: "layouts/project" diff --git a/app/views/layouts/snippets.html.haml b/app/views/layouts/snippets.html.haml index d9c90d4fcef..02ca3ee7a28 100644 --- a/app/views/layouts/snippets.html.haml +++ b/app/views/layouts/snippets.html.haml @@ -1,8 +1,3 @@ -- page_title 'Snippets' -- if current_user - - header_title "Snippets", user_snippets_path(current_user) -- else - - header_title 'Snippets', snippets_path -- sidebar "dashboard" +- header_title "Snippets", snippets_path = render template: "layouts/application" diff --git a/app/views/notify/new_user_email.html.haml b/app/views/notify/new_user_email.html.haml index 4feacdaacff..6b9b42dcf37 100644 --- a/app/views/notify/new_user_email.html.haml +++ b/app/views/notify/new_user_email.html.haml @@ -13,4 +13,4 @@ %p = link_to "Click here to set your password", edit_password_url(@user, reset_password_token: @token) %p - = reset_token_expire_message + = raw reset_token_expire_message diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 767fe2e0e9a..cd7b1b0fe03 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -1,9 +1,7 @@ - page_title "Account" -%h3.page-title - = page_title -%p.light - Change your username and basic account settings. -%hr +- header_title page_title, profile_account_path +- @blank_container = true + - if current_user.ldap_user? .alert.alert-info Some options are unavailable for LDAP accounts @@ -69,7 +67,7 @@ - button_based_providers.each do |provider| .btn-group = link_to provider_image_tag(provider), user_omniauth_authorize_path(provider), method: :post, class: "btn btn-lg #{'active' if auth_active?(provider)}", "data-no-turbolink" => "true" - + - if auth_active?(provider) = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'btn btn-lg' do = icon('close') diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml index 3a3e6e1b1c4..2342936a5d5 100644 --- a/app/views/profiles/applications.html.haml +++ b/app/views/profiles/applications.html.haml @@ -1,13 +1,12 @@ - page_title "Applications" -%h3.page-title - = page_title -%p.light +- header_title page_title, applications_profile_path + +.gray-content-block.top-block - if user_oauth_applications? - Manage applications that can use GitLab as an OAuth provider, + Manage applications that can use GitLab as an OAuth provider, and applications that you've authorized to use your account. - else Manage applications that you've authorized to use your account. -%hr - if user_oauth_applications? .oauth-applications diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml index 698d6037428..8fdba45b193 100644 --- a/app/views/profiles/audit_log.html.haml +++ b/app/views/profiles/audit_log.html.haml @@ -1,5 +1,8 @@ - page_title "Audit Log" -%h3.page-title Audit Log -%p.light History of authentications +- header_title page_title, audit_log_profile_path -= render 'event_table', events: @events
\ No newline at end of file +.gray-content-block.top-block + History of authentications + +.prepend-top-default += render 'event_table', events: @events diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 66812872c41..1d140347a5f 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -1,12 +1,10 @@ - page_title "Emails" -%h3.page-title - = page_title -%p.light - Control emails linked to your account -%hr +- header_title page_title, profile_emails_path +.gray-content-block.top-block + Control emails linked to your account -%ul +%ul.prepend-top-default %li Your %b Primary Email diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index 06655f7ba3a..14adba1c797 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -1,11 +1,12 @@ - page_title "SSH Keys" -%h3.page-title - = page_title +- header_title page_title, profile_keys_path + +.gray-content-block.top-block .pull-right = link_to "Add SSH Key", new_profile_key_path, class: "btn btn-new" -%p.light - Before you can add an SSH key you need to - = link_to "generate it.", help_page_path("ssh", "README") -%hr + .oneline + Before you can add an SSH key you need to + = link_to "generate it.", help_page_path("ssh", "README") +.prepend-top-default = render 'key_table' diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index db7fa2eabe3..8eebd96b674 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -1,10 +1,10 @@ - page_title "Notifications" -%h3.page-title - = page_title -%p.light +- header_title page_title, profile_notifications_path + +.gray-content-block.top-block These are your global notification settings. -%hr +.prepend-top-default = form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications form-horizontal global-notifications-form' } do |f| -if @user.errors.any? %div.alert.alert-danger @@ -33,7 +33,7 @@ = f.label :notification_level, value: Notification::N_MENTION do = f.radio_button :notification_level, Notification::N_MENTION .level-title - Mention + On Mention %p You will receive notifications only for comments in which you were @mentioned .radio diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index 399ae98adf9..fab7c45c9b2 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -1,13 +1,13 @@ - page_title "Password" -%h3.page-title - = page_title -%p.light +- header_title page_title, edit_profile_password_path + +.gray-content-block.top-block - if @user.password_automatically_set? Set your password. - else Change your password or recover your current one. -%hr -.update-password + +.update-password.prepend-top-default = form_for @user, url: profile_password_path, method: :put, html: { class: 'form-horizontal' } do |f| %div %p.slead diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml index 9c6204963e0..d165f758c81 100644 --- a/app/views/profiles/passwords/new.html.haml +++ b/app/views/profiles/passwords/new.html.haml @@ -12,7 +12,7 @@ %ul - @user.errors.full_messages.each do |msg| %li= msg - + - unless @user.password_automatically_set? .form-group = f.label :current_password, class: 'control-label' diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index aa0361a0a1b..60289bfe7cd 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -1,11 +1,11 @@ - page_title 'Preferences' -%h3.page-title - = page_title -%p.light +- header_title page_title, profile_preferences_path +- @blank_container = true + +.alert.alert-help These settings allow you to customize the appearance and behavior of the site. They are saved with your account and will persist to any device you use to access the site. -%hr = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'js-preferences-form form-horizontal'} do |f| .panel.panel-default.application-theme diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index c519e52e596..47412e2ef0c 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -1,14 +1,9 @@ -- page_title "Profile" -%h3.page-title - = page_title -%p.light +.gray-content-block.top-block This information will appear on your profile. - if current_user.ldap_user? Some options are unavailable for LDAP accounts -%hr - - +.prepend-top-default = form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit_user form-horizontal" }, authenticity_token: true do |f| -if @user.errors.any? %div.alert.alert-danger diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index b93036e78e6..8c0980369fd 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -7,24 +7,28 @@ - if @project.description.present? = markdown(@project.description, pipeline: :description) - - .project-repo-buttons - = render 'projects/buttons/star' - - - unless empty_repo - = render 'projects/buttons/fork' - - - if forked_from_project = @project.forked_from_project - = link_to project_path(forked_from_project), class: 'btn' do - = icon("code-fork fw") - Forked from + - if forked_from_project = @project.forked_from_project + %p + Forked from + = link_to project_path(forked_from_project) do = forked_from_project.namespace.try(:name) - - if can? current_user, :download_code, @project - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn', rel: 'nofollow' do - = icon('download fw') - Download - = render 'projects/buttons/dropdown' - = render "shared/clone_panel" + .project-repo-buttons + .split-one + = render 'projects/buttons/star' + + - unless empty_repo + = render 'projects/buttons/fork' + + = render "shared/clone_panel" + .split-repo-buttons + - unless empty_repo + - if can? current_user, :download_code, @project + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn', rel: 'nofollow' do + = icon('download fw') + + = render 'projects/buttons/dropdown' + = render 'projects/buttons/notifications' + diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml index 30622d8a910..f0a3e416db7 100644 --- a/app/views/projects/_last_push.html.haml +++ b/app/views/projects/_last_push.html.haml @@ -1,14 +1,15 @@ - if event = last_push_event - if show_last_push_widget?(event) - .hidden-xs.center - .slead - %span You pushed to - = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do - %strong= event.ref_name - branch - #{time_ago_with_tooltip(event.created_at)} - %div - = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do - Create Merge Request - %hr + .gray-content-block.top-block.clear-block.hidden-xs + .event-last-push + .event-last-push-text + %span You pushed to + = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do + %strong= event.ref_name + branch + #{time_ago_with_tooltip(event.created_at)} + + .pull-right + = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do + Create Merge Request diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml index 5038edb95ed..5bc1999ec9d 100644 --- a/app/views/projects/_readme.html.haml +++ b/app/views/projects/_readme.html.haml @@ -5,7 +5,7 @@ - if can?(current_user, :push_code, @project) = link_to namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light' do - %i.fa.fa-pencil + %i.fa-align.fa.fa-pencil .wiki = cache(readme_cache_key) do = render_readme(readme) diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml index 65674913bb0..555ed76426d 100644 --- a/app/views/projects/activity.html.haml +++ b/app/views/projects/activity.html.haml @@ -1 +1,4 @@ +- page_title "Activity" +- header_title project_title(@project, "Activity", activity_project_path(@project)) + = render 'projects/activity' diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index c1ec42aefca..6518c4173e1 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -1,4 +1,6 @@ - page_title "Blame", @blob.path, @ref +- header_title project_title(@project, "Files", project_files_path(@project)) + %h3.page-title Blame view #tree-holder.tree-holder diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index 13f8271b979..373b3a0c5b0 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -17,6 +17,6 @@ tree_join(@commit.sha, @path)), class: 'btn btn-sm' - if allowed_tree_edit? - = button_tag class: 'remove-blob btn btn-sm btn-remove', - 'data-toggle' => 'modal', 'data-target' => '#modal-remove-blob' do - Remove + .btn-group{ role: "group" } + %button.btn.btn-default{ 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } Replace + %button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Remove diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 65c3ab10e02..b4c7d8b9b71 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -15,7 +15,7 @@ - else = link_to title, '#' -%ul.blob-commit-info.well.hidden-xs +%ul.blob-commit-info.hidden-xs - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) = render blob_commit, project: @project diff --git a/app/views/projects/blob/_header_title.html.haml b/app/views/projects/blob/_header_title.html.haml new file mode 100644 index 00000000000..78c5ef20a5f --- /dev/null +++ b/app/views/projects/blob/_header_title.html.haml @@ -0,0 +1 @@ +- header_title project_title(@project, "Files", project_files_path(@project)) diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml new file mode 100644 index 00000000000..1a1df127703 --- /dev/null +++ b/app/views/projects/blob/_upload.html.haml @@ -0,0 +1,28 @@ +#modal-upload-blob.modal + .modal-dialog + .modal-content + .modal-header + %a.close{href: "#", "data-dismiss" => "modal"} × + %h3.page-title #{title} + %p.light + From branch + %strong= @ref + .modal-body + = form_tag form_path, method: method, class: 'blob-file-upload-form-js form-horizontal' do + .dropzone + .dropzone-previews.blob-upload-dropzone-previews + %p.dz-message.light + Attach a file by drag & drop or + = link_to 'click to upload', '#', class: "markdown-selector" + %br + .dropzone-alerts{class: "alert alert-danger data", style: "display:none"} + = render 'shared/commit_message_container', params: params, + placeholder: placeholder + .form-group + .col-sm-offset-2.col-sm-10 + = button_tag button_title, class: 'btn btn-small btn-primary btn-upload-file', id: 'submit-all' + = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" + +:coffeescript + disableButtonIfEmptyField $('.blob-file-upload-form-js').find('#commit_message'), '.btn-upload-file' + new BlobFileDropzone($('.blob-file-upload-form-js'), '#{method}') diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index a12cd660fc1..a811adc5094 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -1,6 +1,8 @@ - page_title "Edit", @blob.path, @ref += render "header_title" + .file-editor - %ul.nav.nav-tabs.js-edit-mode + %ul.center-top-menu.no-bottom.js-edit-mode %li.active = link_to '#editor' do %i.fa.fa-edit diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 7c2a4fece94..1950586b112 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -1,5 +1,14 @@ -- page_title "New File", @ref -%h3.page-title New file +- page_title "New File", @path.presence, @ref += render "header_title" + +.gray-content-block.top-block + Create a new file or + = link_to 'upload', '#modal-upload-blob', + { class: 'upload-link', 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} + an existing one + += render 'projects/blob/upload', title: 'Upload', placeholder: 'Upload new file', button_title: 'Upload file', form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post + .file-editor = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal form-new-file js-requires-input') do = render 'projects/blob/editor', ref: @ref diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index bd2fc43633c..fa4be4a1bc4 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -1,4 +1,5 @@ - page_title @blob.path, @ref += render "header_title" = render 'projects/last_push' @@ -10,3 +11,8 @@ - if allowed_tree_edit? = render 'projects/blob/remove' + + - title = "Replace #{@blob.name}" + = render 'projects/blob/upload', title: title, placeholder: title, + button_title: 'Replace file', form_path: namespace_project_update_blob_path(@project.namespace, @project, @id), + method: :put diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 6e2dc2d2710..03ade02a0c8 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -1,4 +1,5 @@ - page_title "Branches" += render "projects/commits/header_title" = render "projects/commits/head" .gray-content-block .pull-right diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index 29e82b93883..f5577042ca4 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -1,4 +1,6 @@ - page_title "New Branch" += render "projects/commits/header_title" + - if @error .alert.alert-danger %button{ type: "button", class: "close", "data-dismiss" => "alert"} × diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index bc7625e8989..4580c912692 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -1,6 +1,6 @@ - if current_user %span.dropdown - %a.dropdown-toggle.btn.btn-new{href: '#', "data-toggle" => "dropdown"} + %a.dropdown-new.btn.btn-new{href: '#', "data-toggle" => "dropdown"} = icon('plus') %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown - if can?(current_user, :create_issue, @project) diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 854c154824d..8f2f631eb7d 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -8,6 +8,5 @@ - else = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn' do = icon('code-fork fw') - Fork %span.count = @project.forks_count diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml new file mode 100644 index 00000000000..4b69a6d7a6f --- /dev/null +++ b/app/views/projects/buttons/_notifications.html.haml @@ -0,0 +1,14 @@ +- return unless @membership + += form_tag profile_notifications_path, method: :put, remote: true, class: 'inline-form', id: 'notification-form' do + = hidden_field_tag :notification_type, 'project' + = hidden_field_tag :notification_id, @membership.id + = hidden_field_tag :notification_level + %span.dropdown + %a.dropdown-new.btn.btn-new#notifications-button{href: '#', "data-toggle" => "dropdown"} + = icon('bell') + = notification_label(@membership) + = icon('angle-down') + %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown + - Notification.project_notification_levels.each do |level| + = notification_list_item(level, @membership) diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index 5d7df5ae099..3501dddefbe 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -1,10 +1,6 @@ - if current_user = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star', method: :post, remote: true do = icon('star fw') - - if current_user.starred?(@project) - Unstar - - else - Star %span.count = @project.star_count @@ -17,6 +13,5 @@ - else = link_to new_user_session_path, class: 'btn has_tooltip star-btn', title: 'You must sign in to star a project' do = icon('star fw') - Star %span.count = @project.star_count diff --git a/app/views/projects/ci_settings/_form.html.haml b/app/views/projects/ci_settings/_form.html.haml new file mode 100644 index 00000000000..9f891f557a9 --- /dev/null +++ b/app/views/projects/ci_settings/_form.html.haml @@ -0,0 +1,103 @@ +%h3.page-title + CI settings +%hr +.bs-callout.help-callout + %p + If you want to test your .gitlab-ci.yml, you can use special tool - #{link_to "Lint", ci_lint_path} + %p + Edit your + #{link_to ".gitlab-ci.yml using web-editor", yaml_web_editor_link(@ci_project)} + += nested_form_for @ci_project, url: namespace_project_ci_settings_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f| + - if @ci_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 + + %fieldset + %legend Build settings + .form-group + = label_tag nil, class: 'control-label' do + Get code + .col-sm-10 + %p Get recent application code using the following command: + .radio + = label_tag do + = f.radio_button :allow_git_fetch, 'false' + %strong git clone + .light Slower but makes sure you have a clean dir before every build + .radio + = label_tag do + = f.radio_button :allow_git_fetch, 'true' + %strong git fetch + .light Faster + .form-group + = f.label :timeout_in_minutes, 'Timeout', class: 'control-label' + .col-sm-10 + = f.number_field :timeout_in_minutes, class: 'form-control', min: '0' + .light per build in minutes + + + %fieldset + %legend Build Schedule + .form-group + = f.label :always_build, 'Schedule build', class: 'control-label' + .col-sm-10 + .checkbox + = f.label :always_build do + = f.check_box :always_build + %span.light Repeat last build after X hours if no builds + .form-group + = f.label :polling_interval, "Build interval", class: 'control-label' + .col-sm-10 + = f.number_field :polling_interval, placeholder: '5', min: '0', class: 'form-control' + .light In hours + + %fieldset + %legend Project settings + .form-group + = f.label :default_ref, "Make tabs for the following branches", class: 'control-label' + .col-sm-10 + = f.text_field :default_ref, class: 'form-control', placeholder: 'master, stable' + .light You will be able to filter builds by the following branches + .form-group + = f.label :public, 'Public mode', class: 'control-label' + .col-sm-10 + .checkbox + = f.label :public do + = f.check_box :public + %span.light Anyone can see project and builds + .form-group + = f.label :coverage_regex, "Test coverage parsing", class: 'control-label' + .col-sm-10 + .input-group + %span.input-group-addon / + = f.text_field :coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered' + %span.input-group-addon / + .light We will use this regular expression to find test coverage output in build trace. Leave blank if you want to disable this feature + .bs-callout.bs-callout-info + %p Below are examples of regex for existing tools: + %ul + %li + Simplecov (Ruby) - + %code \(\d+.\d+\%\) covered + %li + pytest-cov (Python) - + %code \d+\%$ + + + + %fieldset + %legend Advanced settings + .form-group + = f.label :token, "CI token", class: 'control-label' + .col-sm-10 + = f.text_field :token, class: 'form-control', placeholder: 'xEeFCaDAB89' + + .form-actions + = f.submit 'Save changes', class: 'btn btn-save' + - unless @ci_project.new_record? + = link_to 'Remove Project', ci_project_path(@ci_project), method: :delete, data: { confirm: 'Project will be removed. Are you sure?' }, class: 'btn btn-danger pull-right' diff --git a/app/views/projects/ci_settings/edit.html.haml b/app/views/projects/ci_settings/edit.html.haml new file mode 100644 index 00000000000..e9040fe4337 --- /dev/null +++ b/app/views/projects/ci_settings/edit.html.haml @@ -0,0 +1,21 @@ +- if @ci_project.generated_yaml_config + %p.alert.alert-danger + CI Jobs are deprecated now, you can #{link_to "download", dumped_yaml_ci_project_path(@ci_project)} + or + %a.preview-yml{:href => "#yaml-content", "data-toggle" => "modal"} preview + yaml file which is based on your old jobs. + Put this file to the root of your project and name it .gitlab-ci.yml + += render 'form' + +- if @ci_project.generated_yaml_config + #yaml-content.modal.fade{"aria-hidden" => "true", "aria-labelledby" => ".gitlab-ci.yml", :role => "dialog", :tabindex => "-1"} + .modal-dialog + .modal-content + .modal-header + %button.close{"aria-hidden" => "true", "data-dismiss" => "modal", :type => "button"} × + %h4.modal-title Content of .gitlab-ci.yml + .modal-body + = text_area_tag :yaml, @ci_project.generated_yaml_config, size: "70x25", class: "form-control" + .modal-footer + %button.btn.btn-default{"data-dismiss" => "modal", :type => "button"} Close diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 2ac79e87b4a..fbf0a9ec0c3 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -38,6 +38,13 @@ - @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 diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 60b112e67d4..f8681024d1b 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -1,4 +1,5 @@ - page_title "#{@commit.title} (#{@commit.short_id})", "Commits" += render "projects/commits/header_title" = render "commit_box" = 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..efad4cb1473 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.id, 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 50c0fd6803d..a849bf84698 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,10 +1,10 @@ %ul.center-top-menu = nav_link(controller: [:commit, :commits]) do - = 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 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 + = 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 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 a01a99458a0..2dd99cc8215 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -1,4 +1,5 @@ - 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") diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index 43d00726c48..02be5a2d07f 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -1,4 +1,5 @@ - page_title "Compare" += render "projects/commits/header_title" = render "projects/commits/head" .gray-content-block diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 8800ffdf482..39755efd2fd 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -1,4 +1,5 @@ - page_title "#{params[:from]}...#{params[:to]}" += render "projects/commits/header_title" = render "projects/commits/head" diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 2f24dc7c909..c5acafa2630 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -15,7 +15,12 @@ .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/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 798f1c47da5..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 "#{h git_user_name}" - git config --global user.email "#{h 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 #{h @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 } - ctx = $("#hour-chart").get(0).getContext("2d"); - new Chart(ctx).Bar(data,{"scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2}) + # get selector by context + ctx = selector.get(0).getContext("2d") + # pointing parent container to make chart.js inherit its width + container = $(selector).parent() - 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} - }] - } + generateChart = -> + selector.attr('width', $(container).width()) + new Chart(ctx).Bar(data, options) + + # enabling auto-resizing + $(window).resize( generateChart ) - ctx = $("#weekday-chart").get(0).getContext("2d"); - new Chart(ctx).Bar(data,{"scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2}) + generateChart() - 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} - }] + 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 = $("#month-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) + + dayData = chartData(#{@commits_per_week_days.keys.to_json}, #{@commits_per_week_days.values.to_json}) + responsiveChart($('#weekday-chart'), dayData) + + 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/_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/index.html.haml b/app/views/projects/issues/index.html.haml index 24314d11404..d6260ab2900 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -1,4 +1,6 @@ - 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") 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 09080642293..5cb814c9ea8 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,4 +1,6 @@ - page_title "#{@issue.title} (##{@issue.iid})", "Issues" += render "header_title" + .issue .issue-details.issuable-details .page-title 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/_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/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 61e04dce5ab..0b0f52c653c 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,4 +1,6 @@ - page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests" += render "header_title" + - if params[:view] == 'parallel' - fluid_layout true 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 d3a576977c2..086298e5af1 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,4 +1,6 @@ - page_title "Merge Requests" += render "header_title" + = render 'projects/last_push' .project-issuable-filter .controls 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/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 4d4e2f68f61..10640f746f0 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -10,7 +10,8 @@ %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" + - 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 = icon("spinner spin") 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/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/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 2b8fd671587..a207385bd43 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,4 +1,5 @@ - page_title "Milestones" += render "header_title" = render 'shared/milestones_filter' .gray-content-block 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/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml index 6daf2d15d07..b8068835b3a 100644 --- a/app/views/projects/notes/_discussion.html.haml +++ b/app/views/projects/notes/_discussion.html.haml @@ -1,5 +1,5 @@ - note = discussion_notes.first -.timeline-entry.prepend-top-default +.timeline-entry .timeline-entry-inner .timeline-icon = link_to user_path(note.author) do 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/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 - -%p.light += render "header_title" + +.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 d5c4ee71978..6a5fc689803 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -14,7 +14,7 @@ .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 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/index.html.haml b/app/views/projects/tags/index.html.haml index 1503a4330f4..85d76eae3b5 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -1,4 +1,5 @@ - page_title "Tags" += render "projects/commits/header_title" = render "projects/commits/head" .gray-content-block 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/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/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 226fd3b2290..6417ef4a38b 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -1,4 +1,6 @@ - page_title "Git Access", "Wiki" += render "header_title" + = render 'nav' .gray-content-block .row diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index 7c81ad53d32..bfbef823b35 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,4 +1,6 @@ -- page_title "History", @page.title, "Wiki" +- page_title "History", @page.title.capitalize, "Wiki" += render "header_title" + = render 'nav' .gray-content-block %h3.page-title diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index 7fb91507eb2..03e6a522b25 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -1,4 +1,6 @@ - page_title "All Pages", "Wiki" += render "header_title" + = render 'nav' .gray-content-block %h3.page-title diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 126810811ec..55fbf5a8b6e 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -1,4 +1,6 @@ -- page_title @page.title, "Wiki" +- page_title @page.title.capitalize, "Wiki" += render "header_title" + = render 'nav' .gray-content-block diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 2cd422e772a..b23b2f0d5eb 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -24,4 +24,4 @@ .input-group-addon .visibility-level-label.has_tooltip{'data-title' => "#{visibility_level_label(project.visibility_level)} project" } = visibility_level_icon(project.visibility_level) - = visibility_level_label(project.visibility_level).downcase + diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 09327d645f3..33ec726e93c 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -9,7 +9,7 @@ = f.label :title, class: 'control-label' do %strong= 'Title *' .col-sm-10 - = f.text_field :title, maxlength: 255, autofocus: true, + = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off', class: 'form-control pad js-gfm-input', required: true - if issuable.is_a?(MergeRequest) @@ -24,7 +24,7 @@ = f.label :description, 'Description', class: 'control-label' .col-sm-10 - = render layout: 'projects/md_preview', locals: { preview_class: "wiki", referenced_users: true } do + = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' .col-sm-12.hint diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 021e3b689a1..16e1d8421de 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -1,14 +1,15 @@ - projects_limit = 20 unless local_assigns[:projects_limit] - avatar = true unless local_assigns[:avatar] == false - stars = true unless local_assigns[:stars] == false +- ci = false unless local_assigns[:ci] == true %ul.projects-list - projects.each_with_index do |project, i| - css_class = (i >= projects_limit) ? 'hide' : nil = render "shared/projects/project", project: project, - avatar: avatar, stars: stars, css_class: css_class + avatar: avatar, stars: stars, css_class: css_class, ci: ci - - if projects.count > projects_limit + - if projects.size > projects_limit %li.bottom.center .light #{projects_limit} of #{pluralize(projects.count, 'project')} displayed. diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 5318c6011f4..e67e5a8a638 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -1,9 +1,10 @@ - avatar = true unless local_assigns[:avatar] == false - stars = true unless local_assigns[:stars] == false +- ci = false unless local_assigns[:ci] == true - css_class = '' unless local_assigns[:css_class] - css_class += " no-description" unless project.description.present? %li.project-row{ class: css_class } - = cache [project.namespace, project, controller.controller_name, controller.action_name, 'v2.1'] do + = cache [project.namespace, project, controller.controller_name, controller.action_name, 'v2.2'] do = link_to project_path(project), class: dom_class(project) do - if avatar .dash-project-avatar @@ -15,8 +16,16 @@ \/ %span.project-name.filter-title = project.name + + .project-controls + - if ci && !project.empty_repo? && project.commit + - if ci_commit = project.ci_commit(project.commit.sha) + = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}", + title: "Build status: #{ci_commit.status}", data: {toggle: 'tooltip', placement: 'left'} do + = ci_status_icon(ci_commit) + - if stars - %span.pull-right.light + %span %i.fa.fa-star = project.star_count - if project.description.present? diff --git a/app/views/snippets/_head.html.haml b/app/views/snippets/_head.html.haml deleted file mode 100644 index 0adf6b91f2c..00000000000 --- a/app/views/snippets/_head.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -%ul.center-top-menu - = nav_link(page: user_snippets_path(current_user), html_options: {class: 'home'}) do - = link_to user_snippets_path(current_user), title: 'Your snippets', data: {placement: 'right'} do - Your Snippets - = nav_link(page: snippets_path) do - = link_to snippets_path, title: 'Explore snippets', data: {placement: 'right'} do - Explore Snippets diff --git a/app/views/snippets/current_user_index.html.haml b/app/views/snippets/current_user_index.html.haml deleted file mode 100644 index d704407c4dd..00000000000 --- a/app/views/snippets/current_user_index.html.haml +++ /dev/null @@ -1,36 +0,0 @@ -- page_title "Your Snippets" -= render 'head' - -.gray-content-block - .pull-right - = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do - Add new snippet - - .oneline - Share code pastes with others out of git repository - -%ul.nav.nav-tabs.prepend-top-20 - = nav_tab :scope, nil do - = link_to user_snippets_path(@user) do - All - %span.badge - = @user.snippets.count - = nav_tab :scope, 'are_private' do - = link_to user_snippets_path(@user, scope: 'are_private') do - Private - %span.badge - = @user.snippets.are_private.count - = nav_tab :scope, 'are_internal' do - = link_to user_snippets_path(@user, scope: 'are_internal') do - Internal - %span.badge - = @user.snippets.are_internal.count - = nav_tab :scope, 'are_public' do - = link_to user_snippets_path(@user, scope: 'are_public') do - Public - %span.badge - = @user.snippets.are_public.count - -.my-snippets - = render 'snippets' - diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml index 3b62dd2a6e1..7e4918a6085 100644 --- a/app/views/snippets/index.html.haml +++ b/app/views/snippets/index.html.haml @@ -1,15 +1,13 @@ -- page_title "Public Snippets" -- if current_user - = render 'head' - -.gray-content-block - - if current_user - .pull-right - = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do - Add new snippet - - .oneline - Public snippets created by you and other users are listed here +- page_title "By #{@user.name}", "Snippets" + +%ol.breadcrumb + %li + = link_to snippets_path do + Snippets + %li + = @user.name + .pull-right.hidden-xs + = link_to user_path(@user) do + #{@user.name} profile page = render 'snippets' - diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index aed00f9caeb..97374e073dc 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -20,10 +20,10 @@ .back-link - if @snippet.author == current_user - = link_to user_snippets_path(current_user) do + = link_to dashboard_snippets_path do ← your snippets - else - = link_to snippets_path do + = link_to explore_snippets_path do ← explore snippets .file-holder diff --git a/app/views/snippets/user_index.html.haml b/app/views/snippets/user_index.html.haml deleted file mode 100644 index 7af5352da34..00000000000 --- a/app/views/snippets/user_index.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- page_title "Snippets", @user.name - -%ol.breadcrumb - %li - = link_to snippets_path do - Snippets - %li - = @user.name - .pull-right.hidden-xs - = link_to user_path(@user) do - #{@user.name} profile page - -= render 'snippets' diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index aa4e8722fb1..37d5dba0330 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -16,8 +16,8 @@ - if @user == current_user .pull-right.hidden-xs = link_to profile_path, class: 'btn btn-sm' do - %i.fa.fa-pencil-square-o - Edit Profile settings + = icon('user') + Profile settings - elsif current_user .pull-right %span.dropdown diff --git a/app/workers/ci/hip_chat_notifier_worker.rb b/app/workers/ci/hip_chat_notifier_worker.rb new file mode 100644 index 00000000000..ebb43570e2a --- /dev/null +++ b/app/workers/ci/hip_chat_notifier_worker.rb @@ -0,0 +1,19 @@ +module Ci + class HipChatNotifierWorker + include Sidekiq::Worker + + def perform(message, options={}) + room = options.delete('room') + token = options.delete('token') + server = options.delete('server') + name = options.delete('service_name') + client_opts = { + api_version: 'v2', + server_url: server + } + + client = HipChat::Client.new(token, client_opts) + client[room].send(name, message, options.symbolize_keys) + end + end +end diff --git a/app/workers/ci/slack_notifier_worker.rb b/app/workers/ci/slack_notifier_worker.rb new file mode 100644 index 00000000000..3bbb9b4bec7 --- /dev/null +++ b/app/workers/ci/slack_notifier_worker.rb @@ -0,0 +1,10 @@ +module Ci + class SlackNotifierWorker + include Sidekiq::Worker + + def perform(webhook_url, message, options={}) + notifier = Slack::Notifier.new(webhook_url) + notifier.ping(message, options) + end + end +end diff --git a/app/workers/ci/web_hook_worker.rb b/app/workers/ci/web_hook_worker.rb new file mode 100644 index 00000000000..0bb83845572 --- /dev/null +++ b/app/workers/ci/web_hook_worker.rb @@ -0,0 +1,9 @@ +module Ci + class WebHookWorker + include Sidekiq::Worker + + def perform(hook_id, data) + Ci::WebHook.find(hook_id).execute data + end + end +end diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb index 8cfb96ef376..5a921a73fe9 100644 --- a/app/workers/email_receiver_worker.rb +++ b/app/workers/email_receiver_worker.rb @@ -4,7 +4,7 @@ class EmailReceiverWorker sidekiq_options queue: :incoming_email def perform(raw) - return unless Gitlab::ReplyByEmail.enabled? + return unless Gitlab::IncomingEmail.enabled? begin Gitlab::Email::Receiver.new(raw).execute diff --git a/app/workers/fork_registration_worker.rb b/app/workers/fork_registration_worker.rb deleted file mode 100644 index fffa8b3a659..00000000000 --- a/app/workers/fork_registration_worker.rb +++ /dev/null @@ -1,12 +0,0 @@ -class ForkRegistrationWorker - include Sidekiq::Worker - - sidekiq_options queue: :default - - def perform(from_project_id, to_project_id, private_token) - from_project = Project.find(from_project_id) - to_project = Project.find(to_project_id) - - from_project.gitlab_ci_service.fork_registration(to_project, private_token) - end -end diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb new file mode 100644 index 00000000000..acd1c43f06b --- /dev/null +++ b/app/workers/repository_fork_worker.rb @@ -0,0 +1,34 @@ +class RepositoryForkWorker + include Sidekiq::Worker + include Gitlab::ShellAdapter + + sidekiq_options queue: :gitlab_shell + + def perform(project_id, source_path, target_path) + project = Project.find_by_id(project_id) + + unless project.present? + logger.error("Project #{project_id} no longer exists!") + return + end + + result = gitlab_shell.fork_repository(source_path, target_path) + + unless result + logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}") + project.import_fail + project.save + return + end + + if project.valid_repo? + ProjectCacheWorker.perform_async(project.id) + project.import_finish + else + project.import_fail + logger.error("Project #{id} had an invalid repository after fork") + end + + project.save + end +end diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index f2ba2e15e7b..ea2808045eb 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -7,22 +7,31 @@ class RepositoryImportWorker def perform(project_id) project = Project.find(project_id) - import_result = gitlab_shell.send(:import_repository, + unless project.import_url == Project::UNKNOWN_IMPORT_URL + import_result = gitlab_shell.send(:import_repository, project.path_with_namespace, project.import_url) - return project.import_fail unless import_result + return project.import_fail unless import_result + else + unless project.create_repository + return project.import_fail + end + end - data_import_result = if project.import_type == 'github' - Gitlab::GithubImport::Importer.new(project).execute - elsif project.import_type == 'gitlab' - Gitlab::GitlabImport::Importer.new(project).execute - elsif project.import_type == 'bitbucket' - Gitlab::BitbucketImport::Importer.new(project).execute - elsif project.import_type == 'google_code' - Gitlab::GoogleCodeImport::Importer.new(project).execute - else - true - end + data_import_result = case project.import_type + when 'github' + Gitlab::GithubImport::Importer.new(project).execute + when 'gitlab' + Gitlab::GitlabImport::Importer.new(project).execute + when 'bitbucket' + Gitlab::BitbucketImport::Importer.new(project).execute + when 'google_code' + Gitlab::GoogleCodeImport::Importer.new(project).execute + when 'fogbugz' + Gitlab::FogbugzImport::Importer.new(project).execute + else + true + end return project.import_fail unless data_import_result Gitlab::BitbucketImport::KeyDeleter.new(project).execute if project.import_type == 'bitbucket' diff --git a/bin/background_jobs b/bin/background_jobs index a4895cf6586..d4578f6a222 100755 --- a/bin/background_jobs +++ b/bin/background_jobs @@ -37,7 +37,7 @@ start_no_deamonize() start_sidekiq() { - 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 -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1 + 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 -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1 } load_ok() diff --git a/bin/ci/upgrade.rb b/bin/ci/upgrade.rb new file mode 100644 index 00000000000..aab4f60ec60 --- /dev/null +++ b/bin/ci/upgrade.rb @@ -0,0 +1,3 @@ +require_relative "../lib/ci/upgrader" + +Ci::Upgrader.new.execute diff --git a/builds/.gitkeep b/builds/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/builds/.gitkeep diff --git a/config/environments/development.rb b/config/environments/development.rb index 03af7f07864..d7d6aed1602 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -24,6 +24,11 @@ Gitlab::Application.configure do # Expands the lines which load the assets # config.assets.debug = true + + # Adds additional error checking when serving assets at runtime. + # Checks for improperly declared sprockets dependencies. + # Raises helpful error messages. + config.assets.raise_runtime_errors = true # For having correct urls in mails config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 9eb99dae456..c7174f86014 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -4,7 +4,7 @@ # ########################### NOTE ##################################### # This file should not receive new settings. All configuration options # -# that do not require application restart are being moved to # +# that do not require an application restart are being moved to # # ApplicationSetting model! # # If you change this file in a Merge Request, please also create # # a MR on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests # @@ -96,10 +96,10 @@ production: &base ## Reply by email # Allow users to comment on issues and merge requests by replying to notification emails. - # For documentation on how to set this up, see http://doc.gitlab.com/ce/reply_by_email/README.md - reply_by_email: + # For documentation on how to set this up, see http://doc.gitlab.com/ce/incoming_email/README.html + incoming_email: enabled: false - address: "replies+%{reply_key}@gitlab.example.com" + address: "incoming+%{key}@gitlab.example.com" ## Gravatar ## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html @@ -110,7 +110,23 @@ production: &base # ssl_url: "https://..." # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon # - # 2. Auth settings + # 2. GitLab CI settings + # ========================== + + gitlab_ci: + # Default project notifications settings: + # + # Send emails only on broken builds (default: true) + # all_broken_builds: true + # + # Add pusher to recipients list (default: false) + # add_pusher: true + + # The location where build traces are stored (default: builds/). Relative paths are relative to Rails.root + # builds_path: builds/ + + # + # 3. Auth settings # ========================== ## LDAP settings @@ -180,6 +196,26 @@ production: &base # user_filter: '' + # LDAP attributes that GitLab will use to create an account for the LDAP user. + # The specified attribute can either be the attribute name as a string (e.g. 'mail'), + # or an array of attribute names to try in order (e.g. ['mail', 'email']). + # Note that the user's LDAP login will always be the attribute specified as `uid` above. + attributes: + # The username will be used in paths for the user's own projects + # (like `gitlab.example.com/username/project`) and when mentioning + # them in issues, merge request and comments (like `@username`). + # If the attribute specified for `username` contains an email address, + # the GitLab username will be the part of the email address before the '@'. + username: ['uid', 'userid', 'sAMAccountName'] + email: ['mail', 'email', 'userPrincipalName'] + + # If no full name could be found at the attribute specified for `name`, + # the full name is determined using the attributes specified for + # `first_name` and `last_name`. + name: 'cn' + first_name: 'givenName' + last_name: 'sn' + # GitLab EE only: add more LDAP servers # Choose an ID made of a-z and 0-9 . This ID will be stored in the database # so that GitLab can remember which LDAP server a user belongs to. @@ -216,28 +252,28 @@ production: &base # arguments, followed by optional 'args' which can be either a hash or an array. # Documentation for this is available at http://doc.gitlab.com/ce/integration/omniauth.html providers: - # - { name: 'google_oauth2', + # - { name: 'google_oauth2', # label: 'Google', - # app_id: 'YOUR_APP_ID', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', # args: { access_type: 'offline', approval_prompt: '' } } - # - { name: 'twitter', - # app_id: 'YOUR_APP_ID', + # - { name: 'twitter', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET' } - # - { name: 'github', + # - { name: 'github', # label: 'GitHub', - # app_id: 'YOUR_APP_ID', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', # args: { scope: 'user:email' } } - # - { name: 'gitlab', + # - { name: 'gitlab', # label: 'GitLab.com', - # app_id: 'YOUR_APP_ID', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', # args: { scope: 'api' } } - # - { name: 'bitbucket', - # app_id: 'YOUR_APP_ID', + # - { name: 'bitbucket', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET' } - # - { name: 'saml', + # - { name: 'saml', # label: 'Our SAML Provider', # args: { # assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', @@ -256,7 +292,7 @@ production: &base # - # 3. Advanced settings + # 4. Advanced settings # ========================== # GitLab Satellites @@ -270,6 +306,7 @@ production: &base path: "tmp/backups" # Relative paths are relative to Rails.root (default: tmp/backups/) # archive_permissions: 0640 # Permissions for the resulting backup.tar file (default: 0600) # keep_time: 604800 # default: 0 (forever) (in seconds) + # pg_schema: public # default: nil, it means that all schemas will be backed up # upload: # # Fog storage connection settings, see http://fog.io/storage/ . # connection: @@ -282,6 +319,8 @@ production: &base # # Use multipart uploads when file size reaches 100MB, see # # http://docs.aws.amazon.com/AmazonS3/latest/dev/uploadobjusingmpu.html # multipart_chunk_size: 104857600 + # # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional + # # encryption: 'AES256' ## GitLab Shell settings gitlab_shell: @@ -315,7 +354,7 @@ production: &base timeout: 10 # - # 4. Extra customization + # 5. Extra customization # ========================== extra: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index c47e5dab27c..4e4a8ecbdb3 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -18,7 +18,19 @@ class Settings < Settingslogic host.start_with?('www.') ? host[4..-1] : host end - private + def build_gitlab_ci_url + if gitlab_on_standard_port? + custom_port = nil + else + custom_port = ":#{gitlab.port}" + end + [ gitlab.protocol, + "://", + gitlab.host, + custom_port, + gitlab.relative_url_root + ].join('') + end def build_gitlab_shell_ssh_path_prefix if gitlab_shell.ssh_port != 22 @@ -97,6 +109,7 @@ if Settings.ldap['enabled'] || Rails.env.test? server['block_auto_created_users'] = false if server['block_auto_created_users'].nil? server['allow_username_or_email_login'] = false if server['allow_username_or_email_login'].nil? server['active_directory'] = true if server['active_directory'].nil? + server['attributes'] = {} if server['attributes'].nil? server['provider_name'] ||= "ldap#{key}".downcase server['provider_class'] = OmniAuth::Utils.camelize(server['provider_name']) end @@ -158,13 +171,24 @@ Settings.gitlab.default_projects_features['snippets'] = false if Settings. Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['repository_downloads_path'] = File.absolute_path(Settings.gitlab['repository_downloads_path'] || 'tmp/repositories', Rails.root) Settings.gitlab['restricted_signup_domains'] ||= [] -Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','git'] +Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'] + + +# +# CI +# +Settings['gitlab_ci'] ||= Settingslogic.new({}) +Settings.gitlab_ci['enabled'] = true if Settings.gitlab_ci['enabled'].nil? +Settings.gitlab_ci['all_broken_builds'] = true if Settings.gitlab_ci['all_broken_builds'].nil? +Settings.gitlab_ci['add_pusher'] = false if Settings.gitlab_ci['add_pusher'].nil? +Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url) +Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci['builds_path'] || "builds/", Rails.root) # # Reply by email # -Settings['reply_by_email'] ||= Settingslogic.new({}) -Settings.reply_by_email['enabled'] = false if Settings.reply_by_email['enabled'].nil? +Settings['incoming_email'] ||= Settingslogic.new({}) +Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil? # # Gravatar @@ -196,6 +220,7 @@ Settings.gitlab_shell['ssh_path_prefix'] ||= Settings.send(:build_gitlab_shell_s # Settings['backup'] ||= Settingslogic.new({}) Settings.backup['keep_time'] ||= 0 +Settings.backup['pg_schema'] = nil Settings.backup['path'] = File.expand_path(Settings.backup['path'] || "tmp/backups/", Rails.root) Settings.backup['archive_permissions'] ||= 0600 Settings.backup['upload'] ||= Settingslogic.new({ 'remote_directory' => nil, 'connection' => nil }) @@ -204,6 +229,7 @@ if Settings.backup['upload']['connection'] Settings.backup['upload']['connection'] = Hash[Settings.backup['upload']['connection'].map { |k, v| [k.to_sym, v] }] end Settings.backup['upload']['multipart_chunk_size'] ||= 104857600 +Settings.backup['upload']['encryption'] ||= nil # # Git diff --git a/config/initializers/3_grit_ext.rb b/config/initializers/3_grit_ext.rb deleted file mode 100644 index 6540ac839cb..00000000000 --- a/config/initializers/3_grit_ext.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'grit' - -Grit::Git.git_binary = Gitlab.config.git.bin_path -Grit::Git.git_timeout = Gitlab.config.git.timeout -Grit::Git.git_max_size = Gitlab.config.git.max_size diff --git a/config/initializers/4_ci_app.rb b/config/initializers/4_ci_app.rb new file mode 100644 index 00000000000..cac8edb32bf --- /dev/null +++ b/config/initializers/4_ci_app.rb @@ -0,0 +1,10 @@ +module GitlabCi + VERSION = Gitlab::VERSION + REVISION = Gitlab::REVISION + + REGISTRATION_TOKEN = SecureRandom.hex(10) + + def self.config + Settings + end +end diff --git a/config/initializers/connection_fix.rb b/config/initializers/connection_fix.rb new file mode 100644 index 00000000000..d831a1838ed --- /dev/null +++ b/config/initializers/connection_fix.rb @@ -0,0 +1,32 @@ +# from http://gist.github.com/238999 +# +# If your workers are inactive for a long period of time, they'll lose +# their MySQL connection. +# +# This hack ensures we re-connect whenever a connection is +# lost. Because, really. why not? +# +# Stick this in RAILS_ROOT/config/initializers/connection_fix.rb (or somewhere similar) +# +# From: +# http://coderrr.wordpress.com/2009/01/08/activerecord-threading-issues-and-resolutions/ + +if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) + module ActiveRecord::ConnectionAdapters + class Mysql2Adapter + alias_method :execute_without_retry, :execute + + def execute(*args) + execute_without_retry(*args) + rescue ActiveRecord::StatementInvalid => e + if e.message =~ /server has gone away/i + warn "Server timed out, retrying" + reconnect! + retry + else + raise e + end + end + end + end +end diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb new file mode 100644 index 00000000000..43adac8b2c6 --- /dev/null +++ b/config/initializers/cookies_serializer.rb @@ -0,0 +1,3 @@ +# Be sure to restart your server when you modify this file. + +Gitlab::Application.config.action_dispatch.cookies_serializer = :hybrid diff --git a/config/initializers/8_default_url_options.rb b/config/initializers/default_url_options.rb index 8fd27b1d88e..f9f88f95db9 100644 --- a/config/initializers/8_default_url_options.rb +++ b/config/initializers/default_url_options.rb @@ -8,4 +8,4 @@ unless Gitlab.config.gitlab_on_standard_port? default_url_options[:port] = Gitlab.config.gitlab.port end -Rails.application.routes.default_url_options = default_url_options +Gitlab::Application.routes.default_url_options = default_url_options diff --git a/config/initializers/7_omniauth.rb b/config/initializers/omniauth.rb index 70ed10e8275..70ed10e8275 100644 --- a/config/initializers/7_omniauth.rb +++ b/config/initializers/omniauth.rb diff --git a/config/initializers/rack_attack.rb.example b/config/initializers/rack_attack.rb.example index b1bbcca1d61..2155ea14562 100644 --- a/config/initializers/rack_attack.rb.example +++ b/config/initializers/rack_attack.rb.example @@ -4,13 +4,13 @@ # If you change this file in a Merge Request, please also create a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests paths_to_be_protected = [ - "#{Rails.application.config.relative_url_root}/users/password", - "#{Rails.application.config.relative_url_root}/users/sign_in", - "#{Rails.application.config.relative_url_root}/api/#{API::API.version}/session.json", - "#{Rails.application.config.relative_url_root}/api/#{API::API.version}/session", - "#{Rails.application.config.relative_url_root}/users", - "#{Rails.application.config.relative_url_root}/users/confirmation", - "#{Rails.application.config.relative_url_root}/unsubscribes/" + "#{Gitlab::Application.config.relative_url_root}/users/password", + "#{Gitlab::Application.config.relative_url_root}/users/sign_in", + "#{Gitlab::Application.config.relative_url_root}/api/#{API::API.version}/session.json", + "#{Gitlab::Application.config.relative_url_root}/api/#{API::API.version}/session", + "#{Gitlab::Application.config.relative_url_root}/users", + "#{Gitlab::Application.config.relative_url_root}/users/confirmation", + "#{Gitlab::Application.config.relative_url_root}/unsubscribes/" ] diff --git a/config/initializers/6_rack_profiler.rb b/config/initializers/rack_profiler.rb index 1d958904e8f..7710eeac453 100644 --- a/config/initializers/6_rack_profiler.rb +++ b/config/initializers/rack_profiler.rb @@ -2,7 +2,7 @@ if Rails.env.development? require 'rack-mini-profiler' # initialization is skipped so trigger it - Rack::MiniProfilerRails.initialize!(Rails.application) + Rack::MiniProfilerRails.initialize!(Gitlab::Application) Rack::MiniProfiler.config.position = 'right' Rack::MiniProfiler.config.start_hidden = false diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index 62a54bc8c63..1b518c3becf 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -24,3 +24,27 @@ end Gitlab::Application.config.secret_token = find_secure_token Gitlab::Application.config.secret_key_base = find_secure_token + +# CI +def generate_new_secure_token + SecureRandom.hex(64) +end + +if Gitlab::Application.secrets.db_key_base.blank? + warn "Missing `db_key_base` for '#{Rails.env}' environment. The secrets will be generated and stored in `config/secrets.yml`" + + all_secrets = YAML.load_file('config/secrets.yml') if File.exist?('config/secrets.yml') + all_secrets ||= {} + + # generate secrets + env_secrets = all_secrets[Rails.env.to_s] || {} + env_secrets['db_key_base'] ||= generate_new_secure_token + all_secrets[Rails.env.to_s] = env_secrets + + # save secrets + File.open('config/secrets.yml', 'w', 0600) do |file| + file.write(YAML.dump(all_secrets)) + end + + Gitlab::Application.secrets.db_key_base = env_secrets['db_key_base'] +end diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 6d274cd95a1..04ed9e90df5 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -2,7 +2,12 @@ require 'gitlab/current_settings' include Gitlab::CurrentSettings -Settings.gitlab['session_expire_delay'] = current_application_settings.session_expire_delay + +# allow it to fail: it may to do so when create_from_defaults is executed before migrations are actually done +begin + Settings.gitlab['session_expire_delay'] = current_application_settings.session_expire_delay +rescue +end Gitlab::Application.config.session_store( :redis_store, # Using the cookie_store would enable session replay attacks. @@ -11,5 +16,5 @@ Gitlab::Application.config.session_store( secure: Gitlab.config.gitlab.https, httponly: true, expire_after: Settings.gitlab['session_expire_delay'] * 60, - path: (Rails.application.config.relative_url_root.nil?) ? '/' : Rails.application.config.relative_url_root + path: (Gitlab::Application.config.relative_url_root.nil?) ? '/' : Gitlab::Application.config.relative_url_root ) diff --git a/config/initializers/4_sidekiq.rb b/config/initializers/sidekiq.rb index e856499732e..e856499732e 100644 --- a/config/initializers/4_sidekiq.rb +++ b/config/initializers/sidekiq.rb diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb index d9042c652bb..e6d5600edb7 100644 --- a/config/initializers/static_files.rb +++ b/config/initializers/static_files.rb @@ -1,4 +1,4 @@ -app = Rails.application +app = Gitlab::Application if app.config.serve_static_assets # The `ActionDispatch::Static` middleware intercepts requests for static files diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index f3db5b7476e..d8bf0878a3d 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -32,10 +32,11 @@ en: send_instructions: 'You will receive an email with instructions about how to reset your password in a few minutes.' updated: 'Your password was changed successfully. You are now signed in.' updated_not_active: 'Your password was changed successfully.' - send_paranoid_instructions: "If your e-mail exists on our database, you will receive a password recovery link on your e-mail" + send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." confirmations: send_instructions: 'You will receive an email with instructions about how to confirm your account in a few minutes.' - send_paranoid_instructions: 'If your e-mail exists on our database, you will receive an email with instructions about how to confirm your account in a few minutes.' + send_paranoid_instructions: 'If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes.' confirmed: 'Your account was successfully confirmed. You are now signed in.' registrations: signed_up: 'Welcome! You have signed up successfully.' @@ -57,4 +58,4 @@ en: reset_password_instructions: subject: 'Reset password instructions' unlock_instructions: - subject: 'Unlock Instructions'
\ No newline at end of file + subject: 'Unlock Instructions' diff --git a/config/mail_room.yml.example b/config/mail_room.yml.example index dd8edfc42eb..82e1a42058e 100644 --- a/config/mail_room.yml.example +++ b/config/mail_room.yml.example @@ -6,6 +6,8 @@ # :port: 993 # # Whether the IMAP server uses SSL # :ssl: true + # # Whether the IMAP server uses StartTLS + # :start_tls: false # # Email account username. Usually the full email address. # :email: "replies@gitlab.example.com" # # Email account password diff --git a/config/routes.rb b/config/routes.rb index 25c286b3083..6d96d8801cd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,6 +2,90 @@ require 'sidekiq/web' require 'api/api' Gitlab::Application.routes.draw do + namespace :ci do + # CI API + Ci::API::API.logger Rails.logger + mount Ci::API::API => '/api' + + resource :lint, only: [:show, :create] + + resources :projects do + collection do + post :add + get :disabled + end + + member do + get :status, to: 'projects#badge' + get :integration + post :toggle_shared_runners + get :dumped_yaml + end + + resources :services, only: [:index, :edit, :update] do + member do + get :test + end + end + + resource :charts, only: [:show] + + resources :refs, constraints: { ref_id: /.*/ }, only: [] do + resources :commits, only: [:show] do + member do + get :status + get :cancel + end + end + end + + resources :builds, only: [:show] do + member do + get :cancel + get :status + post :retry + end + end + + resources :web_hooks, only: [:index, :create, :destroy] do + member do + get :test + end + end + + resources :runner_projects, only: [:create, :destroy] + + resources :events, only: [:index] + end + + resource :user_sessions do + get :auth + get :callback + end + + namespace :admin do + resources :runners, only: [:index, :show, :update, :destroy] do + member do + put :assign_all + get :resume + get :pause + end + end + + resources :events, only: [:index] + + resources :projects do + resources :runner_projects + end + + resources :builds, only: :index + + resource :application_settings, only: [:show, :update] + end + + root to: 'projects#index' + end + use_doorkeeper do controllers applications: 'oauth/applications', authorized_applications: 'oauth/authorized_applications', @@ -99,6 +183,15 @@ Gitlab::Application.routes.draw do get :new_user_map, path: :user_map post :create_user_map, path: :user_map end + + resource :fogbugz, only: [:create, :new], controller: :fogbugz do + get :status + post :callback + get :jobs + + get :new_user_map, path: :user_map + post :create_user_map, path: :user_map + end end # @@ -134,6 +227,7 @@ Gitlab::Application.routes.draw do end resources :groups, only: [:index] + resources :snippets, only: [:index] root to: 'projects#trending' end @@ -158,6 +252,7 @@ Gitlab::Application.routes.draw do put :unblock put :unlock put :confirm + post :login_as patch :disable_two_factor delete 'remove/:email_id', action: 'remove_email', as: 'remove_email' end @@ -201,6 +296,8 @@ Gitlab::Application.routes.draw do resources :services end + resources :labels + root to: 'dashboard#index' end @@ -252,24 +349,25 @@ Gitlab::Application.routes.draw do # # Dashboard Area # - resource :dashboard, controller: 'dashboard', only: [:show] do - member do - get :issues - get :merge_requests - get :activity - end + resource :dashboard, controller: 'dashboard', only: [] do + get :issues + get :merge_requests + get :activity scope module: :dashboard do resources :milestones, only: [:index, :show] resources :groups, only: [:index] + resources :snippets, only: [:index] - resources :projects, only: [] do + resources :projects, only: [:index] do collection do get :starred end end end + + root to: "dashboard/projects#index" end # @@ -293,7 +391,7 @@ Gitlab::Application.routes.draw do end end - resources :projects, constraints: { id: /[^\/]+/ }, only: [:new, :create] + resources :projects, constraints: { id: /[^\/]+/ }, only: [:index, :new, :create] devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks, registrations: :registrations , passwords: :passwords, sessions: :sessions, confirmations: :confirmations } @@ -301,7 +399,7 @@ Gitlab::Application.routes.draw do get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error end - root to: "root#show" + root to: "root#index" # # Project Area @@ -345,6 +443,16 @@ Gitlab::Application.routes.draw do to: 'blob#destroy', constraints: { id: /.+/, format: false } ) + put( + '/blob/*id', + to: 'blob#update', + constraints: { id: /.+/, format: false } + ) + post( + '/blob/*id', + to: 'blob#create', + constraints: { id: /.+/, format: false } + ) end scope do @@ -394,6 +502,7 @@ Gitlab::Application.routes.draw do resources :graphs, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } do member do get :commits + get :ci end end @@ -479,6 +588,9 @@ Gitlab::Application.routes.draw do resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } resources :tags, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } resources :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } + resource :variables, only: [:show, :update] + resources :triggers, only: [:index, :create, :destroy] + resource :ci_settings, only: [:edit, :update, :destroy] resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do member do @@ -534,8 +646,14 @@ Gitlab::Application.routes.draw do get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ } end end - end + resources :runners, only: [:index, :edit, :update, :destroy, :show] do + member do + get :resume + get :pause + end + end + end end end diff --git a/config/schedule.rb b/config/schedule.rb new file mode 100644 index 00000000000..8122f7cc69c --- /dev/null +++ b/config/schedule.rb @@ -0,0 +1,8 @@ +# Use this file to easily define all of your cron jobs. +# +# If you make changes to this file, please create also an issue on +# https://gitlab.com/gitlab-org/omnibus-gitlab/issues . This is necessary +# because the omnibus packages manage cron jobs using Chef instead of Whenever. +every 1.hour do + rake "ci:schedule_builds" +end diff --git a/config/secrets.yml.example b/config/secrets.yml.example new file mode 100644 index 00000000000..6b408ac6031 --- /dev/null +++ b/config/secrets.yml.example @@ -0,0 +1,12 @@ +production: + # db_key_base is used to encrypt for Variables. Ensure that you don't lose it. + # If you change or lose this key you will be unable to access variables stored in database. + # Make sure the secret is at least 30 characters and all random, + # no regular words or you'll be exposed to dictionary attacks. + # db_key_base: + +development: + db_key_base: development + +test: + db_key_base: test diff --git a/config/sidekiq.yml.example b/config/sidekiq.yml.example new file mode 100644 index 00000000000..c691db67c6c --- /dev/null +++ b/config/sidekiq.yml.example @@ -0,0 +1,2 @@ +-- +:concurrency: 5
\ No newline at end of file diff --git a/db/fixtures/production/001_admin.rb b/db/fixtures/production/001_admin.rb index 1c8740f6ba9..b0c0b6450f6 100644 --- a/db/fixtures/production/001_admin.rb +++ b/db/fixtures/production/001_admin.rb @@ -19,7 +19,7 @@ admin = User.create( admin.projects_limit = 10000 admin.admin = true admin.save! -admin.confirm! +admin.confirm if admin.valid? puts %Q[ diff --git a/db/migrate/20150817163600_deduplicate_user_identities.rb b/db/migrate/20150817163600_deduplicate_user_identities.rb index fab669c2905..fceffc48018 100644 --- a/db/migrate/20150817163600_deduplicate_user_identities.rb +++ b/db/migrate/20150817163600_deduplicate_user_identities.rb @@ -1,7 +1,7 @@ class DeduplicateUserIdentities < ActiveRecord::Migration def change execute 'DROP TABLE IF EXISTS tt_migration_DeduplicateUserIdentities;' - execute 'CREATE TEMPORARY TABLE tt_migration_DeduplicateUserIdentities AS SELECT id,provider,user_id FROM identities;' + execute 'CREATE TABLE tt_migration_DeduplicateUserIdentities AS SELECT id,provider,user_id FROM identities;' execute 'DELETE FROM identities WHERE id NOT IN ( SELECT MIN(id) FROM tt_migration_DeduplicateUserIdentities GROUP BY user_id, provider);' execute 'DROP TABLE IF EXISTS tt_migration_DeduplicateUserIdentities;' end diff --git a/db/migrate/20150826001931_add_ci_tables.rb b/db/migrate/20150826001931_add_ci_tables.rb new file mode 100644 index 00000000000..c4f51363e57 --- /dev/null +++ b/db/migrate/20150826001931_add_ci_tables.rb @@ -0,0 +1,190 @@ +class AddCiTables < ActiveRecord::Migration + def change + create_table "ci_application_settings", force: true do |t| + t.boolean "all_broken_builds" + t.boolean "add_pusher" + t.datetime "created_at" + t.datetime "updated_at" + end + + create_table "ci_builds", force: true do |t| + t.integer "project_id" + t.string "status" + t.datetime "finished_at" + t.text "trace" + t.datetime "created_at" + t.datetime "updated_at" + t.datetime "started_at" + t.integer "runner_id" + t.float "coverage" + t.integer "commit_id" + t.text "commands" + t.integer "job_id" + t.string "name" + t.boolean "deploy", default: false + t.text "options" + t.boolean "allow_failure", default: false, null: false + t.string "stage" + t.integer "trigger_request_id" + end + + add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree + add_index "ci_builds", ["project_id", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree + add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree + add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree + + create_table "ci_commits", force: true do |t| + t.integer "project_id" + t.string "ref" + t.string "sha" + t.string "before_sha" + t.text "push_data" + t.datetime "created_at" + t.datetime "updated_at" + t.boolean "tag", default: false + t.text "yaml_errors" + t.datetime "committed_at" + end + + add_index "ci_commits", ["project_id", "committed_at"], name: "index_ci_commits_on_project_id_and_committed_at", using: :btree + add_index "ci_commits", ["project_id", "sha"], name: "index_ci_commits_on_project_id_and_sha", using: :btree + add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree + add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree + + create_table "ci_events", force: true do |t| + t.integer "project_id" + t.integer "user_id" + t.integer "is_admin" + t.text "description" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "ci_events", ["created_at"], name: "index_ci_events_on_created_at", using: :btree + add_index "ci_events", ["is_admin"], name: "index_ci_events_on_is_admin", using: :btree + add_index "ci_events", ["project_id"], name: "index_ci_events_on_project_id", using: :btree + + create_table "ci_jobs", force: true do |t| + t.integer "project_id", null: false + t.text "commands" + t.boolean "active", default: true, null: false + t.datetime "created_at" + t.datetime "updated_at" + t.string "name" + t.boolean "build_branches", default: true, null: false + t.boolean "build_tags", default: false, null: false + t.string "job_type", default: "parallel" + t.string "refs" + t.datetime "deleted_at" + end + + add_index "ci_jobs", ["deleted_at"], name: "index_ci_jobs_on_deleted_at", using: :btree + add_index "ci_jobs", ["project_id"], name: "index_ci_jobs_on_project_id", using: :btree + + create_table "ci_projects", force: true do |t| + t.string "name", null: false + t.integer "timeout", default: 3600, null: false + t.datetime "created_at" + t.datetime "updated_at" + t.string "token" + t.string "default_ref" + t.string "path" + t.boolean "always_build", default: false, null: false + t.integer "polling_interval" + t.boolean "public", default: false, null: false + t.string "ssh_url_to_repo" + t.integer "gitlab_id" + t.boolean "allow_git_fetch", default: true, null: false + t.string "email_recipients", default: "", null: false + t.boolean "email_add_pusher", default: true, null: false + t.boolean "email_only_broken_builds", default: true, null: false + t.string "skip_refs" + t.string "coverage_regex" + t.boolean "shared_runners_enabled", default: false + t.text "generated_yaml_config" + end + + create_table "ci_runner_projects", force: true do |t| + t.integer "runner_id", null: false + t.integer "project_id", null: false + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree + add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree + + create_table "ci_runners", force: true do |t| + t.string "token" + t.datetime "created_at" + t.datetime "updated_at" + t.string "description" + t.datetime "contacted_at" + t.boolean "active", default: true, null: false + t.boolean "is_shared", default: false + t.string "name" + t.string "version" + t.string "revision" + t.string "platform" + t.string "architecture" + end + + create_table "ci_services", force: true do |t| + t.string "type" + t.string "title" + t.integer "project_id", null: false + t.datetime "created_at" + t.datetime "updated_at" + t.boolean "active", default: false, null: false + t.text "properties" + end + + add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree + + create_table "ci_sessions", force: true do |t| + t.string "session_id", null: false + t.text "data" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "ci_sessions", ["session_id"], name: "index_ci_sessions_on_session_id", using: :btree + add_index "ci_sessions", ["updated_at"], name: "index_ci_sessions_on_updated_at", using: :btree + + create_table "ci_trigger_requests", force: true do |t| + t.integer "trigger_id", null: false + t.text "variables" + t.datetime "created_at" + t.datetime "updated_at" + t.integer "commit_id" + end + + create_table "ci_triggers", force: true do |t| + t.string "token" + t.integer "project_id", null: false + t.datetime "deleted_at" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree + + create_table "ci_variables", force: true do |t| + t.integer "project_id", null: false + t.string "key" + t.text "value" + t.text "encrypted_value" + t.string "encrypted_value_salt" + t.string "encrypted_value_iv" + end + + add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree + + create_table "ci_web_hooks", force: true do |t| + t.string "url", null: false + t.integer "project_id", null: false + t.datetime "created_at" + t.datetime "updated_at" + end + end +end diff --git a/db/migrate/20150902001023_add_template_to_label.rb b/db/migrate/20150902001023_add_template_to_label.rb new file mode 100644 index 00000000000..bd381a97b69 --- /dev/null +++ b/db/migrate/20150902001023_add_template_to_label.rb @@ -0,0 +1,5 @@ +class AddTemplateToLabel < ActiveRecord::Migration + def change + add_column :labels, :template, :boolean, default: false + end +end
\ No newline at end of file diff --git a/db/migrate/20150914215247_add_ci_tags.rb b/db/migrate/20150914215247_add_ci_tags.rb new file mode 100644 index 00000000000..df3390e8a82 --- /dev/null +++ b/db/migrate/20150914215247_add_ci_tags.rb @@ -0,0 +1,23 @@ +class AddCiTags < ActiveRecord::Migration + def change + create_table "ci_taggings", force: true do |t| + t.integer "tag_id" + t.integer "taggable_id" + t.string "taggable_type" + t.integer "tagger_id" + t.string "tagger_type" + t.string "context", limit: 128 + t.datetime "created_at" + end + + add_index "ci_taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "ci_taggings_idx", unique: true, using: :btree + add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree + + create_table "ci_tags", force: true do |t| + t.string "name" + t.integer "taggings_count", default: 0 + end + + add_index "ci_tags", ["name"], name: "index_ci_tags_on_name", unique: true, using: :btree + end +end diff --git a/db/migrate/20150915001905_enable_ssl_verification_by_default.rb b/db/migrate/20150915001905_enable_ssl_verification_by_default.rb new file mode 100644 index 00000000000..6e924262a13 --- /dev/null +++ b/db/migrate/20150915001905_enable_ssl_verification_by_default.rb @@ -0,0 +1,5 @@ +class EnableSslVerificationByDefault < ActiveRecord::Migration + def change + change_column :web_hooks, :enable_ssl_verification, :boolean, default: true + end +end diff --git a/db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb b/db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb new file mode 100644 index 00000000000..90ce6c2db3d --- /dev/null +++ b/db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb @@ -0,0 +1,8 @@ +class EnableSslVerificationForWebHooks < ActiveRecord::Migration + def up + execute("UPDATE web_hooks SET enable_ssl_verification = true") + end + + def down + end +end diff --git a/db/migrate/20150916114643_add_help_page_text_to_application_settings.rb b/db/migrate/20150916114643_add_help_page_text_to_application_settings.rb new file mode 100644 index 00000000000..37a27f11935 --- /dev/null +++ b/db/migrate/20150916114643_add_help_page_text_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddHelpPageTextToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :help_page_text, :text + end +end diff --git a/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb b/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb new file mode 100644 index 00000000000..78d9e5f61a1 --- /dev/null +++ b/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb @@ -0,0 +1,5 @@ +class AddIndexForCommittedAtAndId < ActiveRecord::Migration + def change + add_index :ci_commits, [:project_id, :committed_at, :id] + end +end diff --git a/db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb b/db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb new file mode 100644 index 00000000000..6cf668a170e --- /dev/null +++ b/db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddCiEnabledToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :ci_enabled, :boolean, null: false, default: true + end +end diff --git a/db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb b/db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb new file mode 100644 index 00000000000..0aad6fe5e6e --- /dev/null +++ b/db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb @@ -0,0 +1,5 @@ +class RemoveInvalidMilestonesFromMergeRequests < ActiveRecord::Migration + def up + execute("UPDATE merge_requests SET milestone_id = NULL where milestone_id NOT IN (SELECT id FROM milestones)") + end +end diff --git a/db/migrate/20150920010715_add_consumed_timestep_to_users.rb b/db/migrate/20150920010715_add_consumed_timestep_to_users.rb new file mode 100644 index 00000000000..c8438b3f6aa --- /dev/null +++ b/db/migrate/20150920010715_add_consumed_timestep_to_users.rb @@ -0,0 +1,5 @@ +class AddConsumedTimestepToUsers < ActiveRecord::Migration + def change + add_column :users, :consumed_timestep, :integer + end +end diff --git a/db/migrate/20150920161119_add_line_code_to_sent_notification.rb b/db/migrate/20150920161119_add_line_code_to_sent_notification.rb new file mode 100644 index 00000000000..d9af4e71751 --- /dev/null +++ b/db/migrate/20150920161119_add_line_code_to_sent_notification.rb @@ -0,0 +1,5 @@ +class AddLineCodeToSentNotification < ActiveRecord::Migration + def change + add_column :sent_notifications, :line_code, :string + end +end diff --git a/db/migrate/20150924125150_add_project_id_to_ci_commit.rb b/db/migrate/20150924125150_add_project_id_to_ci_commit.rb new file mode 100644 index 00000000000..1a761fe0f86 --- /dev/null +++ b/db/migrate/20150924125150_add_project_id_to_ci_commit.rb @@ -0,0 +1,5 @@ +class AddProjectIdToCiCommit < ActiveRecord::Migration + def up + add_column :ci_commits, :gl_project_id, :integer + end +end diff --git a/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb b/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb new file mode 100644 index 00000000000..2be57b6062e --- /dev/null +++ b/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb @@ -0,0 +1,6 @@ +class MigrateProjectIdForCiCommits < ActiveRecord::Migration + def up + subquery = 'SELECT gitlab_id FROM ci_projects WHERE ci_projects.id = ci_commits.project_id' + execute("UPDATE ci_commits SET gl_project_id=(#{subquery}) WHERE gl_project_id IS NULL") + end +end diff --git a/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb b/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb new file mode 100644 index 00000000000..8d47dac6441 --- /dev/null +++ b/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb @@ -0,0 +1,9 @@ +class AddNullToNameForCiProjects < ActiveRecord::Migration + def up + change_column_null :ci_projects, :name, true + end + + def down + change_column_null :ci_projects, :name, false + end +end diff --git a/db/migrate/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb index 2b7afae6d7c..73605d4c5e3 100644 --- a/db/migrate/limits_to_mysql.rb +++ b/db/migrate/limits_to_mysql.rb @@ -6,5 +6,9 @@ class LimitsToMysql < ActiveRecord::Migration change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647 change_column :snippets, :content, :text, limit: 2147483647 change_column :notes, :st_diff, :text, limit: 2147483647 + + # CI + change_column :ci_builds, :trace, :text, limit: 1073741823 + change_column :ci_commits, :push_data, :text, limit: 16777215 end end diff --git a/db/schema.rb b/db/schema.rb index 36568dd4edd..4ce6cee86e5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150824002011) do +ActiveRecord::Schema.define(version: 20150930095736) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -45,6 +45,8 @@ ActiveRecord::Schema.define(version: 20150824002011) do t.string "after_sign_out_path" t.integer "session_expire_delay", default: 10080, null: false t.text "import_sources" + t.text "help_page_text" + t.boolean "ci_enabled", default: true, null: false end create_table "audit_events", force: true do |t| @@ -72,6 +74,215 @@ ActiveRecord::Schema.define(version: 20150824002011) do t.string "font" end + create_table "ci_application_settings", force: true do |t| + t.boolean "all_broken_builds" + t.boolean "add_pusher" + t.datetime "created_at" + t.datetime "updated_at" + end + + create_table "ci_builds", force: true do |t| + t.integer "project_id" + t.string "status" + t.datetime "finished_at" + t.text "trace" + t.datetime "created_at" + t.datetime "updated_at" + t.datetime "started_at" + t.integer "runner_id" + t.float "coverage" + t.integer "commit_id" + t.text "commands" + t.integer "job_id" + t.string "name" + t.boolean "deploy", default: false + t.text "options" + t.boolean "allow_failure", default: false, null: false + t.string "stage" + t.integer "trigger_request_id" + end + + add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree + add_index "ci_builds", ["project_id", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree + add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree + add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree + + create_table "ci_commits", force: true do |t| + t.integer "project_id" + t.string "ref" + t.string "sha" + t.string "before_sha" + t.text "push_data" + t.datetime "created_at" + t.datetime "updated_at" + t.boolean "tag", default: false + t.text "yaml_errors" + t.datetime "committed_at" + t.integer "gl_project_id" + end + + add_index "ci_commits", ["project_id", "committed_at", "id"], name: "index_ci_commits_on_project_id_and_committed_at_and_id", using: :btree + add_index "ci_commits", ["project_id", "committed_at"], name: "index_ci_commits_on_project_id_and_committed_at", using: :btree + add_index "ci_commits", ["project_id", "sha"], name: "index_ci_commits_on_project_id_and_sha", using: :btree + add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree + add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree + + create_table "ci_events", force: true do |t| + t.integer "project_id" + t.integer "user_id" + t.integer "is_admin" + t.text "description" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "ci_events", ["created_at"], name: "index_ci_events_on_created_at", using: :btree + add_index "ci_events", ["is_admin"], name: "index_ci_events_on_is_admin", using: :btree + add_index "ci_events", ["project_id"], name: "index_ci_events_on_project_id", using: :btree + + create_table "ci_jobs", force: true do |t| + t.integer "project_id", null: false + t.text "commands" + t.boolean "active", default: true, null: false + t.datetime "created_at" + t.datetime "updated_at" + t.string "name" + t.boolean "build_branches", default: true, null: false + t.boolean "build_tags", default: false, null: false + t.string "job_type", default: "parallel" + t.string "refs" + t.datetime "deleted_at" + end + + add_index "ci_jobs", ["deleted_at"], name: "index_ci_jobs_on_deleted_at", using: :btree + add_index "ci_jobs", ["project_id"], name: "index_ci_jobs_on_project_id", using: :btree + + create_table "ci_projects", force: true do |t| + t.string "name" + t.integer "timeout", default: 3600, null: false + t.datetime "created_at" + t.datetime "updated_at" + t.string "token" + t.string "default_ref" + t.string "path" + t.boolean "always_build", default: false, null: false + t.integer "polling_interval" + t.boolean "public", default: false, null: false + t.string "ssh_url_to_repo" + t.integer "gitlab_id" + t.boolean "allow_git_fetch", default: true, null: false + t.string "email_recipients", default: "", null: false + t.boolean "email_add_pusher", default: true, null: false + t.boolean "email_only_broken_builds", default: true, null: false + t.string "skip_refs" + t.string "coverage_regex" + t.boolean "shared_runners_enabled", default: false + t.text "generated_yaml_config" + end + + create_table "ci_runner_projects", force: true do |t| + t.integer "runner_id", null: false + t.integer "project_id", null: false + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree + add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree + + create_table "ci_runners", force: true do |t| + t.string "token" + t.datetime "created_at" + t.datetime "updated_at" + t.string "description" + t.datetime "contacted_at" + t.boolean "active", default: true, null: false + t.boolean "is_shared", default: false + t.string "name" + t.string "version" + t.string "revision" + t.string "platform" + t.string "architecture" + end + + create_table "ci_services", force: true do |t| + t.string "type" + t.string "title" + t.integer "project_id", null: false + t.datetime "created_at" + t.datetime "updated_at" + t.boolean "active", default: false, null: false + t.text "properties" + end + + add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree + + create_table "ci_sessions", force: true do |t| + t.string "session_id", null: false + t.text "data" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "ci_sessions", ["session_id"], name: "index_ci_sessions_on_session_id", using: :btree + add_index "ci_sessions", ["updated_at"], name: "index_ci_sessions_on_updated_at", using: :btree + + create_table "ci_taggings", force: true do |t| + t.integer "tag_id" + t.integer "taggable_id" + t.string "taggable_type" + t.integer "tagger_id" + t.string "tagger_type" + t.string "context", limit: 128 + t.datetime "created_at" + end + + add_index "ci_taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "ci_taggings_idx", unique: true, using: :btree + add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree + + create_table "ci_tags", force: true do |t| + t.string "name" + t.integer "taggings_count", default: 0 + end + + add_index "ci_tags", ["name"], name: "index_ci_tags_on_name", unique: true, using: :btree + + create_table "ci_trigger_requests", force: true do |t| + t.integer "trigger_id", null: false + t.text "variables" + t.datetime "created_at" + t.datetime "updated_at" + t.integer "commit_id" + end + + create_table "ci_triggers", force: true do |t| + t.string "token" + t.integer "project_id", null: false + t.datetime "deleted_at" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree + + create_table "ci_variables", force: true do |t| + t.integer "project_id", null: false + t.string "key" + t.text "value" + t.text "encrypted_value" + t.string "encrypted_value_salt" + t.string "encrypted_value_iv" + end + + add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree + + create_table "ci_web_hooks", force: true do |t| + t.string "url", null: false + t.integer "project_id", null: false + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "deploy_keys_projects", force: true do |t| t.integer "deploy_key_id", null: false t.integer "project_id", null: false @@ -186,6 +397,7 @@ ActiveRecord::Schema.define(version: 20150824002011) do t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" + t.boolean "template", default: false end add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree @@ -412,6 +624,7 @@ ActiveRecord::Schema.define(version: 20150824002011) do t.integer "recipient_id" t.string "commit_id" t.string "reply_key", null: false + t.string "line_code" end add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree @@ -538,6 +751,7 @@ ActiveRecord::Schema.define(version: 20150824002011) do t.string "public_email", default: "", null: false t.integer "dashboard", default: 0 t.integer "project_view", default: 0 + t.integer "consumed_timestep" end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree @@ -573,7 +787,7 @@ ActiveRecord::Schema.define(version: 20150824002011) do t.boolean "merge_requests_events", default: false, null: false t.boolean "tag_push_events", default: false t.boolean "note_events", default: false, null: false - t.boolean "enable_ssl_verification", default: false + t.boolean "enable_ssl_verification", default: true end add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree diff --git a/doc/README.md b/doc/README.md index 337c4e6a62d..a0ff856ebb6 100644 --- a/doc/README.md +++ b/doc/README.md @@ -15,6 +15,23 @@ - [Web hooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. +## CI Documentation + ++ [Quick Start](ci/quick_start/README.md) ++ [Configuring project (.gitlab-ci.yml)](ci/yaml/README.md) ++ [Configuring runner](ci/runners/README.md) ++ [Configuring deployment](ci/deployment/README.md) ++ [Using Docker Images](ci/docker/using_docker_images.md) ++ [Using Docker Build](ci/docker/using_docker_build.md) ++ [Using Variables](ci/variables/README.md) + +### CI Examples + ++ [Test and deploy Ruby applications to Heroku](ci/examples/test-and-deploy-ruby-application-to-heroku.md) ++ [Test and deploy Python applications to Heroku](ci/examples/test-and-deploy-python-application-to-heroku.md) ++ [Test Clojure applications](ci/examples/test-clojure-application.md) ++ Help your favorite programming language and GitLab by sending a merge request with a guide for that language. + ## Administrator documentation - [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough. @@ -29,7 +46,13 @@ - [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed. - [Update](update/README.md) Update guides to upgrade your installation. - [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page. -- [Reply by email](reply_by_email/README.md) Allow users to comment on issues and merge requests by replying to notification emails. +- [Reply by email](incoming_email/README.md) Allow users to comment on issues and merge requests by replying to notification emails. +- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE. + +### Administrator documentation + ++ [User permissions](permissions/permissions.md) ++ [API](api/README.md) ## Contributor documentation diff --git a/doc/api/projects.md b/doc/api/projects.md index 10533c73a31..96485857035 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -515,6 +515,8 @@ Parameters: "push_events": "true", "issues_events": "true", "merge_requests_events": "true", + "note_events": "true", + "enable_ssl_verification": "true", "created_at": "2012-10-12T17:04:47Z" } ``` @@ -535,6 +537,8 @@ Parameters: - `issues_events` - Trigger hook on issues events - `merge_requests_events` - Trigger hook on merge_requests events - `tag_push_events` - Trigger hook on push_tag events +- `note_events` - Trigger hook on note events +- `enable_ssl_verification` - Do SSL verification when triggering the hook ### Edit project hook @@ -553,6 +557,8 @@ Parameters: - `issues_events` - Trigger hook on issues events - `merge_requests_events` - Trigger hook on merge_requests events - `tag_push_events` - Trigger hook on push_tag events +- `note_events` - Trigger hook on note events +- `enable_ssl_verification` - Do SSL verification when triggering the hook ### Delete project hook diff --git a/doc/api/services.md b/doc/api/services.md index bc5049dd302..7d45b2cf463 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -27,6 +27,14 @@ Delete Asana service for a project. DELETE /projects/:id/services/asana ``` +### Get Asana service settings + +Get Asana service settings for a project. + +``` +GET /projects/:id/services/asana +``` + ## Assembla Project Management Software (Source Commits Endpoint) @@ -52,6 +60,14 @@ Delete Assembla service for a project. DELETE /projects/:id/services/assembla ``` +### Get Assembla service settings + +Get Assembla service settings for a project. + +``` +GET /projects/:id/services/assembla +``` + ## Atlassian Bamboo CI A continuous integration and build server @@ -81,6 +97,14 @@ Delete Atlassian Bamboo CI service for a project. DELETE /projects/:id/services/bamboo ``` +### Get Atlassian Bamboo CI service settings + +Get Atlassian Bamboo CI service settings for a project. + +``` +GET /projects/:id/services/bamboo +``` + ## Buildkite Continuous integration and deployments @@ -107,6 +131,14 @@ Delete Buildkite service for a project. DELETE /projects/:id/services/buildkite ``` +### Get Buildkite service settings + +Get Buildkite service settings for a project. + +``` +GET /projects/:id/services/buildkite +``` + ## Campfire Simple web-based real-time group chat @@ -133,6 +165,14 @@ Delete Campfire service for a project. DELETE /projects/:id/services/campfire ``` +### Get Campfire service settings + +Get Campfire service settings for a project. + +``` +GET /projects/:id/services/campfire +``` + ## Custom Issue Tracker Custom issue tracker @@ -161,6 +201,14 @@ Delete Custom Issue Tracker service for a project. DELETE /projects/:id/services/custom-issue-tracker ``` +### Get Custom Issue Tracker service settings + +Get Custom Issue Tracker service settings for a project. + +``` +GET /projects/:id/services/custom-issue-tracker +``` + ## Drone CI Drone is a Continuous Integration platform built on Docker, written in Go @@ -187,6 +235,14 @@ Delete Drone CI service for a project. DELETE /projects/:id/services/drone-ci ``` +### Get Drone CI service settings + +Get Drone CI service settings for a project. + +``` +GET /projects/:id/services/drone-ci +``` + ## Emails on push Email the commits and diff of each push to a list of recipients. @@ -213,6 +269,14 @@ Delete Emails on push service for a project. DELETE /projects/:id/services/emails-on-push ``` +### Get Emails on push service settings + +Get Emails on push service settings for a project. + +``` +GET /projects/:id/services/emails-on-push +``` + ## External Wiki Replaces the link to the internal wiki with a link to an external wiki. @@ -237,6 +301,14 @@ Delete External Wiki service for a project. DELETE /projects/:id/services/external-wiki ``` +### Get External Wiki service settings + +Get External Wiki service settings for a project. + +``` +GET /projects/:id/services/external-wiki +``` + ## Flowdock Flowdock is a collaboration web app for technical teams. @@ -261,6 +333,14 @@ Delete Flowdock service for a project. DELETE /projects/:id/services/flowdock ``` +### Get Flowdock service settings + +Get Flowdock service settings for a project. + +``` +GET /projects/:id/services/flowdock +``` + ## Gemnasium Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities. @@ -286,6 +366,14 @@ Delete Gemnasium service for a project. DELETE /projects/:id/services/gemnasium ``` +### Get Gemnasium service settings + +Get Gemnasium service settings for a project. + +``` +GET /projects/:id/services/gemnasium +``` + ## GitLab CI Continuous integration server from GitLab @@ -312,6 +400,14 @@ Delete GitLab CI service for a project. DELETE /projects/:id/services/gitlab-ci ``` +### Get GitLab CI service settings + +Get GitLab CI service settings for a project. + +``` +GET /projects/:id/services/gitlab-ci +``` + ## HipChat Private group chat and IM @@ -341,6 +437,14 @@ Delete HipChat service for a project. DELETE /projects/:id/services/hipchat ``` +### Get HipChat service settings + +Get HipChat service settings for a project. + +``` +GET /projects/:id/services/hipchat +``` + ## Irker (IRC gateway) Send IRC messages, on update, to a list of recipients through an Irker gateway. @@ -371,6 +475,14 @@ Delete Irker (IRC gateway) service for a project. DELETE /projects/:id/services/irker ``` +### Get Irker (IRC gateway) service settings + +Get Irker (IRC gateway) service settings for a project. + +``` +GET /projects/:id/services/irker +``` + ## JIRA Jira issue tracker @@ -400,6 +512,14 @@ Delete JIRA service for a project. DELETE /projects/:id/services/jira ``` +### Get JIRA service settings + +Get JIRA service settings for a project. + +``` +GET /projects/:id/services/jira +``` + ## PivotalTracker Project Management Software (Source Commits Endpoint) @@ -424,6 +544,14 @@ Delete PivotalTracker service for a project. DELETE /projects/:id/services/pivotaltracker ``` +### Get PivotalTracker service settings + +Get PivotalTracker service settings for a project. + +``` +GET /projects/:id/services/pivotaltracker +``` + ## Pushover Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop. @@ -452,6 +580,14 @@ Delete Pushover service for a project. DELETE /projects/:id/services/pushover ``` +### Get Pushover service settings + +Get Pushover service settings for a project. + +``` +GET /projects/:id/services/pushover +``` + ## Redmine Redmine issue tracker @@ -479,6 +615,14 @@ Delete Redmine service for a project. DELETE /projects/:id/services/redmine ``` +### Get Redmine service settings + +Get Redmine service settings for a project. + +``` +GET /projects/:id/services/redmine +``` + ## Slack A team communication tool for the 21st century @@ -505,6 +649,14 @@ Delete Slack service for a project. DELETE /projects/:id/services/slack ``` +### Get Slack service settings + +Get Slack service settings for a project. + +``` +GET /projects/:id/services/slack +``` + ## JetBrains TeamCity CI A continuous integration and build server @@ -534,3 +686,11 @@ Delete JetBrains TeamCity CI service for a project. DELETE /projects/:id/services/teamcity ``` +### Get JetBrains TeamCity CI service settings + +Get JetBrains TeamCity CI service settings for a project. + +``` +GET /projects/:id/services/teamcity +``` + diff --git a/doc/ci/README.md b/doc/ci/README.md new file mode 100644 index 00000000000..97325069ceb --- /dev/null +++ b/doc/ci/README.md @@ -0,0 +1,23 @@ +## GitLab CI Documentation + +### User documentation + ++ [Quick Start](quick_start/README.md) ++ [Configuring project (.gitlab-ci.yml)](yaml/README.md) ++ [Configuring runner](runners/README.md) ++ [Configuring deployment](deployment/README.md) ++ [Using Docker Images](docker/using_docker_images.md) ++ [Using Docker Build](docker/using_docker_build.md) ++ [Using Variables](variables/README.md) + +### Examples + ++ [Test and deploy Ruby applications to Heroku](examples/test-and-deploy-ruby-application-to-heroku.md) ++ [Test and deploy Python applications to Heroku](examples/test-and-deploy-python-application-to-heroku.md) ++ [Test Clojure applications](examples/test-clojure-application.md) ++ Help your favorite programming language and GitLab by sending a merge request with a guide for that language. + +### Administrator documentation + ++ [User permissions](permissions/README.md) ++ [API](api/README.md) diff --git a/doc/ci/api/README.md b/doc/ci/api/README.md new file mode 100644 index 00000000000..33c5b172e98 --- /dev/null +++ b/doc/ci/api/README.md @@ -0,0 +1,86 @@ +# GitLab CI API + +## Resources + +- [Projects](projects.md) +- [Runners](runners.md) +- [Commits](commits.md) +- [Builds](builds.md) + + +## Authentication + +GitLab CI API uses different types of authentication depends on what API you use. +Each API document has section with information about authentication you need to use. + +GitLab CI API has 4 authentication methods: + +* GitLab user token & GitLab url +* GitLab CI project token +* GitLab CI runners registration token +* GitLab CI runner token + + +### Authentication #1: GitLab user token & GitLab url + +Authentication is done by +sending the `private-token` of a valid user and the `url` of an +authorized Gitlab instance via a query string along with the API +request: + + GET http://gitlab.example.com/ci/api/v1/projects?private_token=QVy1PB7sTxfy4pqfZM1U&url=http://demo.gitlab.com/ + +If preferred, you may instead send the `private-token` as a header in +your request: + + curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" "http://gitlab.example.com/ci/api/v1/projects?url=http://demo.gitlab.com/" + + +### Authentication #2: GitLab CI project token + +Each project in GitLab CI has it own token. +It can be used to get project commits and builds information. +You can use project token only for certain project. + +### Authentication #3: GitLab CI runners registration token + +This token is not persisted and is generated on each application start. +It can be used only for registering new runners in system. You can find it on +GitLab CI Runners web page https://gitlab-ci.example.com/admin/runners + +### Authentication #4: GitLab CI runner token + +Every GitLab CI runner has it own token that allow it to receive and update +GitLab CI builds. This token exists of internal purposes and should be used only +by runners + +## JSON + +All API requests are serialized using JSON. You don't need to specify +`.json` at the end of API URL. + +## Status codes + +The API is designed to return different status codes according to context and action. In this way if a request results in an error the caller is able to get insight into what went wrong, e.g. status code `400 Bad Request` is returned if a required attribute is missing from the request. The following list gives an overview of how the API functions generally behave. + +API request types: + +- `GET` requests access one or more resources and return the result as JSON +- `POST` requests return `201 Created` if the resource is successfully created and return the newly created resource as JSON +- `GET`, `PUT` and `DELETE` return `200 OK` if the resource is accessed, modified or deleted successfully, the (modified) result is returned as JSON +- `DELETE` requests are designed to be idempotent, meaning a request a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind it is the user is not really interested if the resource existed before or not. + +The following list shows the possible return codes for API requests. + +Return values: + +- `200 OK` - The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON +- `201 Created` - The `POST` request was successful and the resource is returned as JSON +- `400 Bad Request` - A required attribute of the API request is missing, e.g. the title of an issue is not given +- `401 Unauthorized` - The user is not authenticated, a valid user token is necessary, see above +- `403 Forbidden` - The request is not allowed, e.g. the user is not allowed to delete a project +- `404 Not Found` - A resource could not be accessed, e.g. an ID for a resource could not be found +- `405 Method Not Allowed` - The request is not supported +- `409 Conflict` - A conflicting resource already exists, e.g. creating a project with a name that already exists +- `422 Unprocessable` - The entity could not be processed +- `500 Server Error` - While handling the request something went wrong on the server side diff --git a/doc/ci/api/builds.md b/doc/ci/api/builds.md new file mode 100644 index 00000000000..3b5008ccdb4 --- /dev/null +++ b/doc/ci/api/builds.md @@ -0,0 +1,41 @@ +# Builds API + +This API used by runners to receive and update builds. + +__Authentication is done by runner token__ + +## Builds + +### Runs oldest pending build by runner + + POST /ci/builds/register + +Parameters: + + * `token` (required) - The unique token of runner + +Returns: + +```json +{ + "id" : 79, + "commands" : "", + "path" : "", + "ref" : "", + "sha" : "", + "project_id" : 6, + "repo_url" : "git@demo.gitlab.com:gitlab/gitlab-shell.git", + "before_sha" : "" +} +``` + + +### Update details of an existing build + + PUT /ci/builds/:id + +Parameters: + + * `id` (required) - The ID of a project + * `state` (optional) - The state of a build + * `trace` (optional) - The trace of a build diff --git a/doc/ci/api/commits.md b/doc/ci/api/commits.md new file mode 100644 index 00000000000..4df7afc6c52 --- /dev/null +++ b/doc/ci/api/commits.md @@ -0,0 +1,101 @@ +# Commits API + +__Authentication is done by GitLab CI project token__ + +## Commits + +### Retrieve all commits per project + +Get list of commits per project + + GET /ci/commits + +Parameters: + + * `project_id` (required) - The ID of a project + * `project_token` (requires) - Project token + * `page` (optional) + * `per_page` (optional) - items per request (default is 20) + +Returns: + +```json +[{ + "id": 3, + "ref": "master", + "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf", + "project_id": 2, + "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898", + "created_at": "2014-11-05T09:46:35.247Z", + "status": "success", + "finished_at": "2014-11-05T09:46:44.254Z", + "duration": 5.062692165374756, + "git_commit_message": "wow\n", + "git_author_name": "Administrator", + "git_author_email": "admin@example.com", + "builds": [{ + "id": 7, + "project_id": 2, + "ref": "master", + "status": "success", + "finished_at": "2014-11-05T09:46:44.254Z", + "created_at": "2014-11-05T09:46:35.259Z", + "updated_at": "2014-11-05T09:46:44.255Z", + "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf", + "started_at": "2014-11-05T09:46:39.192Z", + "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898", + "runner_id": 1, + "coverage": null, + "commit_id": 3 + }] +}] +``` + +### Create commit + +Inform GitLab CI about new commit you want it to build. + +__If commit already exists in GitLab CI it will not be created__ + + + POST /ci/commits + +Parameters: + + * `project_id` (required) - The ID of a project + * `project_token` (requires) - Project token + * `data` (required) - Push data. For example see comment in `lib/api/commits.rb` + +Returns: + +```json +{ + "id": 3, + "ref": "master", + "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf", + "project_id": 2, + "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898", + "created_at": "2014-11-05T09:46:35.247Z", + "status": "success", + "finished_at": "2014-11-05T09:46:44.254Z", + "duration": 5.062692165374756, + "git_commit_message": "wow\n", + "git_author_name": "Administrator", + "git_author_email": "admin@example.com", + "builds": [{ + "id": 7, + "project_id": 2, + "ref": "master", + "status": "success", + "finished_at": "2014-11-05T09:46:44.254Z", + "created_at": "2014-11-05T09:46:35.259Z", + "updated_at": "2014-11-05T09:46:44.255Z", + "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf", + "started_at": "2014-11-05T09:46:39.192Z", + "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898", + "runner_id": 1, + "coverage": null, + "commit_id": 3 + }] +} +``` diff --git a/doc/ci/api/projects.md b/doc/ci/api/projects.md new file mode 100644 index 00000000000..5585191e826 --- /dev/null +++ b/doc/ci/api/projects.md @@ -0,0 +1,149 @@ +# Projects API + +This API is intended to aid in the setup and configuration of +projects on Gitlab CI. + +__Authentication is done by GitLab user token & GitLab url__ + +## Projects + +### List Authorized Projects + +Lists all projects that the authenticated user has access to. + +``` +GET /ci/projects +``` + +Returns: + +```json + [ + { + "id" : 271, + "name" : "gitlabhq", + "timeout" : 1800, + "token" : "iPWx6WM4lhHNedGfBpPJNP", + "default_ref" : "master", + "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell", + "path" : "gitlab/gitlab-shell", + "always_build" : false, + "polling_interval" : null, + "public" : false, + "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git", + "gitlab_id" : 3 + }, + { + "id" : 272, + "name" : "gitlab-ci", + "timeout" : 1800, + "token" : "iPWx6WM4lhHNedGfBpPJNP", + "default_ref" : "master", + "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell", + "path" : "gitlab/gitlab-shell", + "always_build" : false, + "polling_interval" : null, + "public" : false, + "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git", + "gitlab_id" : 4 + } +] +``` + +### List Owned Projects + +Lists all projects that the authenticated user owns. + +``` +GET /ci/projects/owned +``` + +Returns: + +```json +[ + { + "id" : 272, + "name" : "gitlab-ci", + "timeout" : 1800, + "token" : "iPWx6WM4lhHNedGfBpPJNP", + "default_ref" : "master", + "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell", + "path" : "gitlab/gitlab-shell", + "always_build" : false, + "polling_interval" : null, + "public" : false, + "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git", + "gitlab_id" : 4 + } +] +``` + +### Single Project + +Returns information about a single project for which the user is +authorized. + + GET /ci/projects/:id + +Parameters: + + * `id` (required) - The ID of the Gitlab CI project + +### Create Project + +Creates a Gitlab CI project using Gitlab project details. + + POST /ci/projects + +Parameters: + + * `name` (required) - The name of the project + * `gitlab_id` (required) - The ID of the project on the Gitlab instance + * `default_ref` (optional) - The branch to run on (default to `master`) + +### Update Project + +Updates a Gitlab CI project using Gitlab project details that the +authenticated user has access to. + + PUT /ci/projects/:id + +Parameters: + + * `name` - The name of the project + * `default_ref` - The branch to run on (default to `master`) + +### Remove Project + +Removes a Gitlab CI project that the authenticated user has access to. + + DELETE /ci/projects/:id + +Parameters: + + * `id` (required) - The ID of the Gitlab CI project + +### Link Project to Runner + +Links a runner to a project so that it can make builds (only via +authorized user). + + POST /ci/projects/:id/runners/:runner_id + +Parameters: + + * `id` (required) - The ID of the Gitlab CI project + * `runner_id` (required) - The ID of the Gitlab CI runner + +### Remove Project from Runner + +Removes a runner from a project so that it can not make builds (only +via authorized user). + + DELETE /ci/projects/:id/runners/:runner_id + +Parameters: + + * `id` (required) - The ID of the Gitlab CI project + * `runner_id` (required) - The ID of the Gitlab CI runner
\ No newline at end of file diff --git a/doc/ci/api/runners.md b/doc/ci/api/runners.md new file mode 100644 index 00000000000..e9f88ee066e --- /dev/null +++ b/doc/ci/api/runners.md @@ -0,0 +1,77 @@ +# Runners API + +## Runners + +### Retrieve all runners + +__Authentication is done by GitLab user token & GitLab url__ + +Used to get information about all runners registered on the Gitlab CI +instance. + + GET /ci/runners + +Returns: + +```json +[ + { + "id" : 85, + "token" : "12b68e90394084703135" + }, + { + "id" : 86, + "token" : "76bf894e969364709864" + }, +] +``` + +### Register a new runner + + +__Authentication is done with a Shared runner registration token or a project Specific runner registration token__ + +Used to make Gitlab CI aware of available runners. + + POST /ci/runners/register + +Parameters: + + * `token` (required) - The registration token. It is 2 types of token you can pass here. + +1. Shared runner registration token +2. Project specific registration token + +Returns: + +```json +{ + "id" : 85, + "token" : "12b68e90394084703135" +} +``` + +### Delete a runner + + +__Authentication is done by runner token__ + +Used to removing runners. + + DELETE /ci/runners/delete + +Parameters: + + * `token` (required) - The runner token. + +Returns: + +```json +{ + "id" : 1, + "token" : "d14963981a428f70121777e50643d1", + "created_at" : "2015-02-26T11:39:39.232Z", + "updated_at" : "2015-02-26T11:39:39.232Z", + "description" : "awesome runner" +} +```
\ No newline at end of file diff --git a/doc/ci/deployment/README.md b/doc/ci/deployment/README.md new file mode 100644 index 00000000000..ffd841ca9e7 --- /dev/null +++ b/doc/ci/deployment/README.md @@ -0,0 +1,98 @@ +## Using Dpl as deployment tool +Dpl (dee-pee-ell) is a deploy tool made for continuous deployment that's developed and used by Travis CI, but can also be used with GitLab CI. + +**We recommend to use Dpl, if you're deploying to any of these of these services: https://github.com/travis-ci/dpl#supported-providers**. + +### Requirements +To use Dpl you need at least Ruby 1.8.7 with ability to install gems. + +### Basic usage +The Dpl can be installed on any machine with: +``` +gem install dpl +``` + +This allows you to test all commands from your shell, rather than having to test it on a CI server. + +If you don't have Ruby installed you can do it on Debian-compatible Linux with: +``` +apt-get update +apt-get install ruby-dev +``` + +The Dpl provides support for vast number of services, including: Heroku, Cloud Foundry, AWS/S3, and more. +To use it simply define provider and any additional parameters required by the provider. + +For example if you want to use it to deploy your application to heroku, you need to specify `heroku` as provider, specify `api-key` and `app`. +There's more and all possible parameters can be found here: https://github.com/travis-ci/dpl#heroku + +``` +staging: + type: deploy + - gem install dpl + - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY +``` + +In the above example we use Dpl to deploy `my-app-staging` to Heroku server with api-key stored in `HEROKU_STAGING_API_KEY` secure variable. + +To use different provider take a look at long list of [Supported Providers](https://github.com/travis-ci/dpl#supported-providers). + +### Using Dpl with Docker +When you use GitLab Runner you most likely configured it to use your server's shell commands. +This means that all commands are run in context of local user (ie. gitlab_runner or gitlab_ci_multi_runner). +It also means that most probably in your Docker container you don't have the Ruby runtime installed. +You will have to install it: +``` +staging: + type: deploy + - apt-get update -yq + - apt-get install -y ruby-dev + - gem install dpl + - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY + only: + - master +``` + +The first line `apt-get update -yq` updates the list of available packages, where second `apt-get install -y ruby-dev` install `Ruby` runtime on system. +The above example is valid for all Debian-compatible systems. + +### Usage in staging and production +It's pretty common in developer workflow to have staging (development) and production environment. +If we consider above example: we would like to deploy `master` branch to `staging` and `all tags` to `production` environment. +The final `.gitlab-ci.yml` for that setup would look like this: + +``` +staging: + type: deploy + - gem install dpl + - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY + only: + - master + +production: + type: deploy + - gem install dpl + - dpl --provider=heroku --app=my-app-production --api-key=$HEROKU_PRODUCTION_API_KEY + only: + - tags +``` + +We created two deploy jobs that are executed on different events: +1. `staging` is executed for all commits that were pushed to `master` branch, +2. `production` is executed for all pushed tags. + +We also use two secure variables: +1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app, +2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app. + +### Storing API keys +In GitLab CI 7.12 a new feature was introduced: Secure Variables. +Secure Variables can added by going to `Project > Variables > Add Variable`. +**This feature requires `gitlab-runner` with version equal or greater than 0.4.0.** +The variables that are defined in the project settings are send along with the build script to the runner. +The secure variables are stored out of the repository. Never store secrets in your projects' .gitlab-ci.yml. +It is also important that secret's value is hidden in the build log. + +You access added variable by prefixing it's name with `$` (on non-Windows runners) or `%` (for Windows Batch runners): +1. `$SECRET_VARIABLE` - use it for non-Windows runners +2. `%SECRET_VARIABLE%` - use it for Windows Batch runners diff --git a/doc/ci/docker/README.md b/doc/ci/docker/README.md new file mode 100644 index 00000000000..84eaf29efd1 --- /dev/null +++ b/doc/ci/docker/README.md @@ -0,0 +1,4 @@ +# Docker integration + ++ [Using Docker Images](using_docker_images.md) ++ [Using Docker Build](using_docker_build.md)
\ No newline at end of file diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md new file mode 100644 index 00000000000..5af27470d2f --- /dev/null +++ b/doc/ci/docker/using_docker_build.md @@ -0,0 +1,111 @@ +# Using Docker Build + +GitLab CI allows you to use Docker Engine to build and test docker-based projects. + +**This also allows to you to use `docker-compose` and other docker-enabled tools.** + +This is one of new trends in Continuous Integration/Deployment to: + +1. create application image, +1. run test against created image, +1. push image to remote registry, +1. deploy server from pushed image + +It's also useful in case when your application already has the `Dockerfile` that can be used to create and test image: +```bash +$ docker build -t my-image dockerfiles/ +$ docker run my-docker-image /script/to/run/tests +$ docker tag my-image my-registry:5000/my-image +$ docker push my-registry:5000/my-image +``` + +However, this requires special configuration of GitLab Runner to enable `docker` support during build. +**This requires running GitLab Runner in privileged mode which can be harmful when untrusted code is run.** + +There are two methods to enable the use of `docker build` and `docker run` during build. + +## 1. Use shell executor + +The simplest approach is to install GitLab Runner in `shell` execution mode. +GitLab Runner then executes build scripts as `gitlab-runner` user. + +1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation). + +1. During GitLab Runner installation select `shell` as method of executing build scripts or use command: + + ```bash + $ sudo gitlab-runner register -n \ + --url http://gitlab.com/ci \ + --token RUNNER_TOKEN \ + --executor shell + --description "My Runner" + ``` + +2. Install Docker on server. + + For more information how to install Docker on different systems checkout the [Supported installations](https://docs.docker.com/installation/). + +3. Add `gitlab-runner` user to `docker` group: + + ```bash + $ sudo usermod -aG docker gitlab-runner + ``` + +4. Verify that `gitlab-runner` has access to Docker: + + ```bash + $ sudo -u gitlab-runner -H docker info + ``` + + You can now verify that everything works by adding `docker info` to `.gitlab-ci.yml`: + ```yaml + before_script: + - docker info + + build_image: + script: + - docker build -t my-docker-image . + - docker run my-docker-image /script/to/run/tests + ``` + +5. You can now use `docker` command and install `docker-compose` if needed. + +6. However, by adding `gitlab-runner` to `docker` group you are effectively granting `gitlab-runner` full root permissions. +For more information please checkout [On Docker security: `docker` group considered harmful](https://www.andreas-jung.com/contents/on-docker-security-docker-group-considered-harmful). + +## 2. Use docker-in-docker executor + +Second approach is to use special Docker image with all tools installed (`docker` and `docker-compose`) and run build script in context of that image in privileged mode. +In order to do that follow the steps: + +1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation). + +1. Register GitLab Runner from command line to use `docker` and `privileged` mode: + + ```bash + $ sudo gitlab-runner register -n \ + --url http://gitlab.com/ci \ + --token RUNNER_TOKEN \ + --executor docker \ + --description "My Docker Runner" \ + --docker-image "gitlab/dind:latest" \ + --docker-privileged + ``` + + The above command will register new Runner to use special [gitlab/dind](https://registry.hub.docker.com/u/gitlab/dind/) image which is provided by GitLab Inc. + The image at the start runs Docker daemon in [docker-in-docker](https://blog.docker.com/2013/09/docker-can-now-run-within-docker/) mode. + +1. You can now use `docker` from build script: + + ```yaml + before_script: + - docker info + + build_image: + script: + - docker build -t my-docker-image . + - docker run my-docker-image /script/to/run/tests + ``` + +1. However, by enabling `--docker-privileged` you are effectively disables all security mechanisms of containers and exposing your host to privilege escalation which can lead to container breakout. +For more information, check out [Runtime privilege](https://docs.docker.com/reference/run/#runtime-privilege-linux-capabilities-and-lxc-configuration).
\ No newline at end of file diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md new file mode 100644 index 00000000000..191e3a8144d --- /dev/null +++ b/doc/ci/docker/using_docker_images.md @@ -0,0 +1,203 @@ +# Using Docker Images +GitLab CI can use [Docker Engine](https://www.docker.com/) to build projects. + +Docker is an open-source project that allows to use predefined images to run applications +in independent "containers" that are run within a single Linux instance. +[Docker Hub](https://registry.hub.docker.com/) have rich database of built images that can be used to build applications. + +Docker when used with GitLab CI runs each build in separate and isolated container using predefined image and always from scratch. +It makes it easier to have simple and reproducible build environment that can also be run on your workstation. +This allows you to test all commands from your shell, rather than having to test them on a CI server. + +### Register Docker runner +To use GitLab Runner with Docker you need to register new runner to use `docker` executor: + +```bash +gitlab-ci-multi-runner register \ + --url "https://gitlab.com/" \ + --registration-token "PROJECT_REGISTRATION_TOKEN" \ + --description "docker-ruby-2.1" \ + --executor "docker" \ + --docker-image ruby:2.1 \ + --docker-postgres latest \ + --docker-mysql latest +``` + +**The registered runner will use `ruby:2.1` image and will run two services (`postgres:latest` and `mysql:latest`) that will be accessible for time of the build.** + +### What is image? +The image is the name of any repository that is present in local Docker Engine or any repository that can be found at [Docker Hub](https://registry.hub.docker.com/). +For more information about the image and Docker Hub please read the [Docker Fundamentals](https://docs.docker.com/introduction/understanding-docker/). + +### What is service? +Service is just another image that is run for time of your build and is linked to your build. This allows you to access the service image during build time. +The service image can run any application, but most common use case is to run some database container, ie.: `mysql`. +It's easier and faster to use existing image, run it as additional container than install `mysql` every time project is built. + +#### How is service linked to the build? +There's good document that describes how Docker linking works: [Linking containers together](https://docs.docker.com/userguide/dockerlinks/). +To summarize: if you add `mysql` as service to your application, the image will be used to create container that is linked to build container. +The service container for MySQL will be accessible under hostname `mysql`. +So, **to access your database service you have to connect to host: `mysql` instead of socket or `localhost`**. + +### How to use other images as services? +You are not limited to have only database services. +You can hand modify `config.toml` to add any image as service found at [Docker Hub](https://registry.hub.docker.com/). +Look for `[runners.docker]` section: +``` +[runners.docker] + image = "ruby:2.1" + services = ["mysql:latest", "postgres:latest"] +``` + +For example you need `wordpress` instance to test some API integration with `Wordpress`. +You can for example use this image: [tutum/wordpress](https://registry.hub.docker.com/u/tutum/wordpress/). +This is image that have fully preconfigured `wordpress` and have `MySQL` server built-in: +``` +[runners.docker] + image = "ruby:2.1" + services = ["mysql:latest", "postgres:latest", "tutum/wordpress:latest"] +``` + +Next time when you run your application the `tutum/wordpress` will be started +and you will have access to it from your build container under hostname: `tutum_wordpress`. + +Alias hostname for the service is made from the image name: +1. Everything after `:` is stripped, +2. '/' is replaced to `_`. + +### Configuring services +Many services accept environment variables, which allow you to easily change database names or set account names depending on the environment. + +GitLab Runner 0.5.0 and up passes all YAML-defined variables to created service containers. + +1. To configure database name for [postgres](https://registry.hub.docker.com/u/library/postgres/) service, +you need to set POSTGRES_DB. + + ```yaml + services: + - postgres + + variables: + POSTGRES_DB: gitlab + ``` + +1. To use [mysql](https://registry.hub.docker.com/u/library/mysql/) service with empty password for time of build, +you need to set MYSQL_ALLOW_EMPTY_PASSWORD. + + ```yaml + services: + - mysql + + variables: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + ``` + +For other possible configuration variables check the +https://registry.hub.docker.com/u/library/mysql/ or https://registry.hub.docker.com/u/library/postgres/ +or README page for any other Docker image. + +**Note: All variables will passed to all service containers. It's not designed to distinguish which variable should go where.** + +### Overwrite image and services +It's possible to overwrite `docker-image` and specify services from `.gitlab-ci.yml`. +If you add to your YAML the `image` and the `services` these parameters +be used instead of the ones that were specified during runner's registration. +``` +image: ruby:2.2 +services: + - postgres:9.3 +before_install: + - bundle install + +test: + script: + - bundle exec rake spec +``` + +It's possible to define image and service per-job: +``` +before_install: + - bundle install + +test:2.1: + image: ruby:2.1 + services: + - postgres:9.3 + script: + - bundle exec rake spec + +test:2.2: + image: ruby:2.2 + services: + - postgres:9.4 + script: + - bundle exec rake spec +``` + +#### How to enable overwriting? +To enable overwriting you have to **enable it first** (it's disabled by default for security reasons). +You can do that by hand modifying runner configuration: `config.toml`. +Please go to section where is `[runners.docker]` definition for your runner. +Add `allowed_images` and `allowed_services` to specify what images are allowed to be picked from `.gitlab-ci.yml`: +``` +[runners.docker] + image = "ruby:2.1" + allowed_images = ["ruby:*", "python:*"] + allowed_services = ["mysql:*", "redis:*"] +``` +This enables you to use in your `.gitlab-ci.yml` any image that matches above wildcards. +You will be able to pick only `ruby` and `python` images. +The same rule can be applied to limit services. + +If you are courageous enough, you can make it fully open and accept everything: +``` +[runners.docker] + image = "ruby:2.1" + allowed_images = ["*", "*/*"] + allowed_services = ["*", "*/*"] +``` + +**It the feature is not enabled, or image isn't allowed the error message will be put into the build log.** + +### How Docker integration works +1. Create any service container: `mysql`, `postgresql`, `mongodb`, `redis`. +1. Create cache container to store all volumes as defined in `config.toml` and `Dockerfile` of build image (`ruby:2.1` as in above example). +1. Create build container and link any service container to build container. +1. Start build container and send build script to the container. +1. Run build script. +1. Checkout code in: `/builds/group-name/project-name/`. +1. Run any step defined in `.gitlab-ci.yml`. +1. Check exit status of build script. +1. Remove build container and all created service containers. + +### How to debug a build locally +1. Create a file with build script: +```bash +$ cat <<EOF > build_script +git clone https://gitlab.com/gitlab-org/gitlab-ci-multi-runner.git /builds/gitlab-org/gitlab-ci-multi-runner +cd /builds/gitlab-org/gitlab-ci-multi-runner +make <- or any other build step +EOF +``` + +1. Create service containers: +``` +$ docker run -d -n service-mysql mysql:latest +$ docker run -d -n service-postgres postgres:latest +``` +This will create two service containers (MySQL and PostgreSQL). + +1. Create a build container and execute script in its context: +``` +$ cat build_script | docker run -n build -i -l mysql:service-mysql -l postgres:service-postgres ruby:2.1 /bin/bash +``` +This will create build container that has two service containers linked. +The build_script is piped using STDIN to bash interpreter which executes the build script in container. + +1. At the end remove all containers: +``` +docker rm -f -v build service-mysql service-postgres +``` +This will forcefully (the `-f` switch) remove build container and service containers +and all volumes (the `-v` switch) that were created with the container creation. diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md new file mode 100644 index 00000000000..e0b9fa0e25d --- /dev/null +++ b/doc/ci/examples/README.md @@ -0,0 +1,5 @@ +# Build script examples + ++ [Test and deploy Ruby Application to Heroku](test-and-deploy-ruby-application-to-heroku.md) ++ [Test and deploy Python Application to Heroku](test-and-deploy-python-application-to-heroku.md) ++ [Test Clojure applications](examples/test-clojure-application.md) diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md new file mode 100644 index 00000000000..036b03dd6b9 --- /dev/null +++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md @@ -0,0 +1,72 @@ +## Test and Deploy a python application +This example will guide you how to run tests in your Python application and deploy it automatically as Heroku application. + +You can checkout the example [source](https://gitlab.com/ayufan/python-getting-started) and check [CI status](https://ci.gitlab.com/projects/4080). + +### Configure project +This is what the `.gitlab-ci.yml` file looks like for this project: +```yaml +test: + script: + # this configures django application to use attached postgres database that is run on `postgres` host + - export DATABASE_URL=postgres://postgres:@postgres:5432/python-test-app + - apt-get update -qy + - apt-get install -y python-dev python-pip + - pip install -r requirements.txt + - python manage.py test + +staging: + type: deploy + script: + - apt-get update -qy + - apt-get install -y ruby-dev + - gem install dpl + - dpl --provider=heroku --app=gitlab-ci-python-test-staging --api-key=$HEROKU_STAGING_API_KEY + only: + - master + +production: + type: deploy + script: + - apt-get update -qy + - apt-get install -y ruby-dev + - gem install dpl + - dpl --provider=heroku --app=gitlab-ci-python-test-prod --api-key=$HEROKU_PRODUCTION_API_KEY + only: + - tags +``` + +This project has three jobs: +1. `test` - used to test rails application, +2. `staging` - used to automatically deploy staging environment every push to `master` branch +3. `production` - used to automatically deploy production environmnet for every created tag + +### Store API keys +You'll need to create two variables in `Project > Variables`: +1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app, +2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app. + +Find your Heroku API key in [Manage Account](https://dashboard.heroku.com/account). + +### Create Heroku application +For each of your environments, you'll need to create a new Heroku application. +You can do this through the [Dashboard](https://dashboard.heroku.com/). + +### Create runner +First install [Docker Engine](https://docs.docker.com/installation/). +To build this project you also need to have [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner). +You can use public runners available on `gitlab.com/ci`, but you can register your own: +``` +gitlab-ci-multi-runner register \ + --non-interactive \ + --url "https://gitlab.com/ci/" \ + --registration-token "PROJECT_REGISTRATION_TOKEN" \ + --description "python-3.2" \ + --executor "docker" \ + --docker-image python:3.2 \ + --docker-postgres latest +``` + +With the command above, you create a runner that uses [python:3.2](https://registry.hub.docker.com/u/library/python/) image and uses [postgres](https://registry.hub.docker.com/u/library/postgres/) database. + +To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password. diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md new file mode 100644 index 00000000000..d2a872f1934 --- /dev/null +++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md @@ -0,0 +1,67 @@ +## Test and Deploy a ruby application +This example will guide you how to run tests in your Ruby application and deploy it automatiacally as Heroku application. + +You can checkout the example [source](https://gitlab.com/ayufan/ruby-getting-started) and check [CI status](https://ci.gitlab.com/projects/4050). + +### Configure project +This is what the `.gitlab-ci.yml` file looks like for this project: +```yaml +test: + script: + - apt-get update -qy + - apt-get install -y nodejs + - bundle install --path /cache + - bundle exec rake db:create RAILS_ENV=test + - bundle exec rake test + +staging: + type: deploy + script: + - gem install dpl + - dpl --provider=heroku --app=gitlab-ci-ruby-test-staging --api-key=$HEROKU_STAGING_API_KEY + only: + - master + +production: + type: deploy + script: + - gem install dpl + - dpl --provider=heroku --app=gitlab-ci-ruby-test-prod --api-key=$HEROKU_PRODUCTION_API_KEY + only: + - tags +``` + +This project has three jobs: +1. `test` - used to test rails application, +2. `staging` - used to automatically deploy staging environment every push to `master` branch +3. `production` - used to automatically deploy production environmnet for every created tag + +### Store API keys +You'll need to create two variables in `Project > Variables`: +1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app, +2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app. + +Find your Heroku API key in [Manage Account](https://dashboard.heroku.com/account). + +### Create Heroku application +For each of your environments, you'll need to create a new Heroku application. +You can do this through the [Dashboard](https://dashboard.heroku.com/). + +### Create runner +First install [Docker Engine](https://docs.docker.com/installation/). +To build this project you also need to have [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner). +You can use public runners available on `gitlab.com/ci`, but you can register your own: +``` +gitlab-ci-multi-runner register \ + --non-interactive \ + --url "https://gitlab.com/ci/" \ + --registration-token "PROJECT_REGISTRATION_TOKEN" \ + --description "ruby-2.1" \ + --executor "docker" \ + --docker-image ruby:2.1 \ + --docker-postgres latest +``` + +With the command above, you create a runner that uses [ruby:2.1](https://registry.hub.docker.com/u/library/ruby/) image and uses [postgres](https://registry.hub.docker.com/u/library/postgres/) database. + +To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password.
\ No newline at end of file diff --git a/doc/ci/examples/test-clojure-application.md b/doc/ci/examples/test-clojure-application.md new file mode 100644 index 00000000000..eaee94a10f1 --- /dev/null +++ b/doc/ci/examples/test-clojure-application.md @@ -0,0 +1,35 @@ +## Test Clojure applications + +This example will guide you how to run tests in your Clojure application. + +You can checkout the example [source](https://gitlab.com/dzaporozhets/clojure-web-application) and check [CI status](https://ci.gitlab.com/projects/6306). + +### Configure project + +This is what the `.gitlab-ci.yml` file looks like for this project: + +```yaml +variables: + POSTGRES_DB: sample-test + DATABASE_URL: "postgresql://postgres@postgres:5432/sample-test" + +before_script: + - apt-get update -y + - apt-get install default-jre postgresql-client -y + - wget https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein + - chmod a+x lein + - export LEIN_ROOT=1 + - PATH=$PATH:. + - lein deps + - lein migratus migrate + +test: + script: + - lein test +``` + +In before script we install JRE and [Leiningen](http://leiningen.org/). +Sample project uses [migratus](https://github.com/yogthos/migratus) library to manage database migrations. +So we added database migration as last step of `before_script` section + +You can use public runners available on `gitlab.com` for testing your application with such configuration. diff --git a/doc/ci/permissions/README.md b/doc/ci/permissions/README.md new file mode 100644 index 00000000000..d77061c14cd --- /dev/null +++ b/doc/ci/permissions/README.md @@ -0,0 +1,24 @@ +# Users Permissions + +GitLab CI relies on user's role on the GitLab. There are three permissions levels on GitLab CI: admin, master, developer, other. + +Admin user can perform any actions on GitLab CI in scope of instance and project. Also user with admin permission can use admin interface. + + + + +| Action | Guest, Reporter | Developer | Master | Admin | +|---------------------------------------|-----------------|-------------|----------|--------| +| See commits and builds | ✓ | ✓ | ✓ | ✓ | +| Retry or cancel build | | ✓ | ✓ | ✓ | +| Remove project | | | ✓ | ✓ | +| Create project | | | ✓ | ✓ | +| Change project configuration | | | ✓ | ✓ | +| Add specific runners | | | ✓ | ✓ | +| Add shared runners | | | | ✓ | +| See events in the system | | | | ✓ | +| Admin interface | | | | ✓ | + + + + diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md new file mode 100644 index 00000000000..a87a1f806fc --- /dev/null +++ b/doc/ci/quick_start/README.md @@ -0,0 +1,119 @@ +# Quick Start + +To start building projects with GitLab CI a few steps needs to be done. + +## 1. Install GitLab and CI + +First you need to have a working GitLab and GitLab CI instance. + +You can omit this step if you use [GitLab.com](http://GitLab.com/). + +## 2. Create repository on GitLab + +Once you login on your GitLab add a new repository where you will store your source code. +Push your application to that repository. + +## 3. Add project to CI + +The next part is to login to GitLab CI. +Point your browser to the URL you have set GitLab or use [gitlab.com/ci](http://gitlab.com/ci/). + +On the first screen you will see a list of GitLab's projects that you have access to: + + + +Click **Add Project to CI**. +This will create project in CI and authorize GitLab CI to fetch sources from GitLab. + +> GitLab CI creates unique token that is used to configure GitLab CI service in GitLab. +> This token allows to access GitLab's repository and configures GitLab to trigger GitLab CI webhook on **Push events** and **Tag push events**. +> You can see that token by going to Project's Settings > Services > GitLab CI. +> You will see there token, the same token is assigned in GitLab CI settings of project. + +## 4. Create project's configuration - .gitlab-ci.yml + +The next: You have to define how your project will be built. +GitLab CI uses [YAML](https://en.wikipedia.org/wiki/YAML) file to store build configuration. +You need to create `.gitlab-ci.yml` in root directory of your repository: + +```yaml +before_script: + - bundle install + +rspec: + script: + - bundle exec rspec + +rubocop: + script: + - bundle exec rubocop +``` + +This is the simplest possible build configuration that will work for most Ruby applications: +1. Define two jobs `rspec` and `rubocop` with two different commands to be executed. +1. Before every job execute commands defined by `before_script`. + +The `.gitlab-ci.yml` defines set of jobs with constrains how and when they should be run. +The jobs are defined as top-level elements with name and always have to contain the `script`. +Jobs are used to create builds, which are then picked by [runners](../runners/README.md) and executed within environment of the runner. +What is important that each job is run independently from each other. + +For more information and complete `.gitlab-ci.yml` syntax, please check the [Configuring project (.gitlab-ci.yml)](../yaml/README.md). + +## 5. Add file and push .gitlab-ci.yml to repository + +Once you created `.gitlab-ci.yml` you should add it to git repository and push it to GitLab. + +```bash +git add .gitlab-ci.yml +git commit +git push origin master +``` + +If you refresh the project's page on GitLab CI you will notice a one new commit: + + + +However the commit has status **pending** which means that commit was not yet picked by runner. + +## 6. Configure runner + +In GitLab CI, Runners run your builds. +A runner is a machine (can be virtual, bare-metal or VPS) that picks up builds through the coordinator API of GitLab CI. + +A runner can be specific to a certain project or serve any project in GitLab CI. +A runner that serves all projects is called a shared runner. +More information about different runner types can be found in [Configuring runner](../runners/README.md). + +To check if you have runners assigned to your project go to **Runners**. You will find there information how to setup project specific runner: + +1. Install GitLab Runner software. Checkout the [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner) section to install it. +1. Specify following URL during runner setup: https://gitlab.com/ci/ +1. Use the following registration token during setup: TOKEN + +If you do it correctly your runner should be shown under **Runners activated for this project**: + + + +### Shared runners + +If you use [gitlab.com/ci](http://gitlab.com/ci/) you can use **Shared runners** provided by GitLab Inc. +These are special virtual machines that are run on GitLab's infrastructure that can build any project. +To enable **Shared runners** you have to go to **Runners** and click **Enable shared runners** for this project. + +## 7. Check status of commit + +If everything went OK and you go to commit, the status of the commit should change from **pending** to either **running**, **success** or **failed**. + + + +You can click **Build ID** to view build log for specific job. + +## 8. Congratulations! + +You managed to build your first project using GitLab CI. +You may need to tune your `.gitlab-ci.yml` file to implement build plan for your project. +A few examples how it can be done you can find on [Examples](../examples/README.md) page. + +GitLab CI also offers **the Lint** tool to verify validity of your `.gitlab-ci.yml` which can be useful to troubleshoot potential problems. +The Lint is available from project's settings or by adding `/lint` to GitLab CI url. diff --git a/doc/ci/quick_start/build_status.png b/doc/ci/quick_start/build_status.png Binary files differnew file mode 100644 index 00000000000..333259e6acd --- /dev/null +++ b/doc/ci/quick_start/build_status.png diff --git a/doc/ci/quick_start/commit_status.png b/doc/ci/quick_start/commit_status.png Binary files differnew file mode 100644 index 00000000000..725b79e6f91 --- /dev/null +++ b/doc/ci/quick_start/commit_status.png diff --git a/doc/ci/quick_start/new_commit.png b/doc/ci/quick_start/new_commit.png Binary files differnew file mode 100644 index 00000000000..3839e893c17 --- /dev/null +++ b/doc/ci/quick_start/new_commit.png diff --git a/doc/ci/quick_start/projects.png b/doc/ci/quick_start/projects.png Binary files differnew file mode 100644 index 00000000000..0b3430a69db --- /dev/null +++ b/doc/ci/quick_start/projects.png diff --git a/doc/ci/quick_start/runners.png b/doc/ci/quick_start/runners.png Binary files differnew file mode 100644 index 00000000000..25b4046bc00 --- /dev/null +++ b/doc/ci/quick_start/runners.png diff --git a/doc/ci/quick_start/runners_activated.png b/doc/ci/quick_start/runners_activated.png Binary files differnew file mode 100644 index 00000000000..c934bd12f41 --- /dev/null +++ b/doc/ci/quick_start/runners_activated.png diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md new file mode 100644 index 00000000000..68dcfe23ffb --- /dev/null +++ b/doc/ci/runners/README.md @@ -0,0 +1,145 @@ +# Runners + +In GitLab CI, Runners run your [yaml](../yaml/README.md). +A runner is an isolated (virtual) machine that picks up builds +through the coordinator API of GitLab CI. + +A runner can be specific to a certain project or serve any project +in GitLab CI. A runner that serves all projects is called a shared runner. + +## Shared vs. Specific Runners + +A runner that is specific only runs for the specified project. A shared runner +can run jobs for every project that has enabled the option +`Allow shared runners`. + +**Shared runners** are useful for jobs that have similar requirements, +between multiple projects. Rather than having multiple runners idling for +many projects, you can have a single or a small number of runners that handle +multiple projects. This makes it easier to maintain and update runners. + +**Specific runners** are useful for jobs that have special requirements or for +projects with a very demand. If a job has certain requirements, you can set +up the specific runner with this in mind, while not having to do this for all +runners. For example, if you want to deploy a certain project, you can setup +a specific runner to have the right credentials for this. + +Projects with high demand of CI activity can also benefit from using specific runners. +By having dedicated runners you are guaranteed that the runner is not being held +up by another project's jobs. + +You can set up a specific runner to be used by multiple projects. The difference +with a shared runner is that you have to enable each project explicitly for +the runner to be able to run its jobs. + +Specific runners do not get shared with forked projects automatically. +A fork does copy the CI settings (jobs, allow shared, etc) of the cloned repository. + +# Creating and Registering a Runner + +There are several ways to create a runner. Only after creation, upon +registration its status as Shared or Specific is determined. + +[See the documentation for](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation) +the different methods of installing a Runner instance. + +After installing the runner, you can either register it as `Shared` or as `Specific`. +You can only register a Shared Runner if you have admin access to the GitLab instance. + +## Registering a Shared Runner + +You can only register a shared runner if you are an admin on the linked +GitLab instance. + +Grab the shared-runner token on the `admin/runners` page of your GitLab CI +instance. + + + +Now simply register the runner as any runner: + +``` +sudo gitlab-runner register +``` + +Note that you will have to enable `Allows shared runners` for each project +that you want to make use of a shared runner. This is by default `off`. + +## Registering a Specific Runner + +Registering a specific can be done in two ways: + +1. Creating a runner with the project registration token +1. Converting a shared runner into a specific runner (one-way, admin only) + +There are several ways to create a runner instance. The steps below only +concern registering the runner on GitLab CI. + +### Registering a Specific Runner with a Project Registration token + +To create a specific runner without having admin rights to the GitLab instance, +visit the project you want to make the runner work for in GitLab CI. + +Click on the runner tab and use the registration token you find there to +setup a specific runner for this project. + + + +To register the runner, run the command below and follow instructions: + +``` +sudo gitlab-runner register +``` + +### Making an existing Shared Runner Specific + +If you are an admin on your GitLab instance, +you can make any shared runner a specific runner, _but you can not +make a specific runner a shared runner_. + +To make a shared runner specific, go to the runner page (`/admin/runners`) +and find your runner. Add any projects on the left to make this runner +run exclusively for these projects, therefore making it a specific runner. + + + +## Using Shared Runners Effectively + +If you are planning to use shared runners, there are several things you +should keep in mind. + +### Use Tags + +You must setup a runner to be able to run all the different types of jobs +that it may encounter on the projects it's shared over. This would be +problematic for large amounts of projects, if it wasn't for tags. + +By tagging a Runner for the types of jobs it can handle, you can make sure +shared runners will only run the jobs they are equipped to run. + +For instance, at GitLab we have runners tagged with "rails" if they contain +the appropriate dependencies to run Rails test suites. + +### Be Careful with Sensitive Information + +If you can run a build on a runner, you can get access to any code it runs +and get the token of the runner. With shared runners, this means that anyone +that runs jobs on the runner, can access anyone else's code that runs on the runner. + +In addition, because you can get access to the runner token, it is possible +to create a clone of a runner and submit false builds, for example. + +The above is easily avoided by restricting the usage of shared runners +on large public GitLab instances and controlling access to your GitLab instance. + +### Forks + +Whenever a project is forked, it copies the settings of the jobs that relate +to it. This means that if you have shared runners setup for a project and +someone forks that project, the shared runners will also serve jobs of this +project. + +# Attack vectors in runners + +Mentioned briefly earlier, but the following things of runners can be exploited. +We're always looking for contributions that can mitigate these [Security Considerations](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md). diff --git a/doc/ci/runners/project_specific.png b/doc/ci/runners/project_specific.png Binary files differnew file mode 100644 index 00000000000..f51ea694e78 --- /dev/null +++ b/doc/ci/runners/project_specific.png diff --git a/doc/ci/runners/shared_runner.png b/doc/ci/runners/shared_runner.png Binary files differnew file mode 100644 index 00000000000..9755144eb08 --- /dev/null +++ b/doc/ci/runners/shared_runner.png diff --git a/doc/ci/runners/shared_to_specific_admin.png b/doc/ci/runners/shared_to_specific_admin.png Binary files differnew file mode 100644 index 00000000000..44a4bef22f7 --- /dev/null +++ b/doc/ci/runners/shared_to_specific_admin.png diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md new file mode 100644 index 00000000000..04c6bf1e3a3 --- /dev/null +++ b/doc/ci/variables/README.md @@ -0,0 +1,95 @@ +## Variables +When receiving a build from GitLab CI, the runner prepares the build environment. +It starts by setting a list of **predefined variables** (Environment Variables) and a list of **user-defined variables** + +The variables can be overwritten. They take precedence over each other in this order: +1. Secure variables +1. YAML-defined variables +1. Predefined variables + +For example, if you define: +1. API_TOKEN=SECURE as Secure Variable +1. API_TOKEN=YAML as YAML-defined variable + +The API_TOKEN will take the Secure Variable value: `SECURE`. + +### Predefined variables (Environment Variables) + +| Variable | Description | +|-------------------------|-------------| +| **CI** | Mark that build is executed in CI environment | +| **GITLAB_CI** | Mark that build is executed in GitLab CI environment | +| **CI_SERVER** | Mark that build is executed in CI environment | +| **CI_SERVER_NAME** | CI server that is used to coordinate builds | +| **CI_SERVER_VERSION** | Not yet defined | +| **CI_SERVER_REVISION** | Not yet defined | +| **CI_BUILD_REF** | The commit revision for which project is built | +| **CI_BUILD_BEFORE_SHA** | The first commit that were included in push request | +| **CI_BUILD_REF_NAME** | The branch or tag name for which project is built | +| **CI_BUILD_ID** | The unique id of the current build that GitLab CI uses internally | +| **CI_BUILD_REPO** | The URL to clone the Git repository | +| **CI_PROJECT_ID** | The unique id of the current project that GitLab CI uses internally | +| **CI_PROJECT_DIR** | The full path where the repository is cloned and where the build is ran | + +Example values: + +```bash +export CI_BUILD_BEFORE_SHA="9df57456fa9de2a6d335ca5edf9750ed812b9df0" +export CI_BUILD_ID="50" +export CI_BUILD_REF="1ecfd275763eff1d6b4844ea3168962458c9f27a" +export CI_BUILD_REF_NAME="master" +export CI_BUILD_REPO="https://gitlab.com/gitlab-org/gitlab-ce.git" +export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce" +export CI_PROJECT_ID="34" +export CI_SERVER="yes" +export CI_SERVER_NAME="GitLab CI" +export CI_SERVER_REVISION="" +export CI_SERVER_VERSION="" +``` + +### YAML-defined variables +**This feature requires GitLab Runner 0.5.0 or higher** + +GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build environment. +The variables are stored in repository and are meant to store non-sensitive project configuration, ie. RAILS_ENV or DATABASE_URL. + +```yaml +variables: + DATABASE_URL: "postgres://postgres@postgres/my_database" +``` + +These variables can be later used in all executed commands and scripts. + +The YAML-defined variables are also set to all created service containers, thus allowing to fine tune them. + +More information about Docker integration can be found in [Using Docker Images](../docker/using_docker_images.md). + +### User-defined variables (Secure Variables) +**This feature requires GitLab Runner 0.4.0 or higher** + +GitLab CI allows you to define per-project **Secure Variables** that are set in build environment. +The secure variables are stored out of the repository (the `.gitlab-ci.yml`). +These variables are securely stored in GitLab CI database and are hidden in the build log. +It's desired method to use them for storing passwords, secret keys or whatever you want. + +Secure Variables can added by going to `Project > Variables > Add Variable`. + +They will be available for all subsequent builds. + +### Use variables +The variables are set as environment variables in build environment and are accessible with normal methods that are used to access such variables. +In most cases the **bash** is used to execute build script. +To access variables (predefined and user-defined) in bash environment, prefix the variable name with `$`: +``` +job_name: + script: + - echo $CI_BUILD_ID +``` + +You can also list all environment variables with `export` command, +but be aware that this will also expose value of all **Secure Variables** in build log: +``` +job_name: + script: + - export +``` diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md new file mode 100644 index 00000000000..4caeccacb7f --- /dev/null +++ b/doc/ci/yaml/README.md @@ -0,0 +1,204 @@ +# Configuration of your builds with .gitlab-ci.yml +From version 7.12, GitLab CI uses a [YAML](https://en.wikipedia.org/wiki/YAML) file (**.gitlab-ci.yml**) for the project configuration. +It is placed in the root of your repository and contains definitions of how your project should be built. + +The YAML file defines a set of jobs with constraints stating when they should be run. +The jobs are defined as top-level elements with a name and always have to contain the `script` clause: + +```yaml +job1: + script: "execute-script-for-job1" + +job2: + script: "execute-script-for-job2" +``` + +The above example is the simplest possible CI configuration with two separate jobs, +where each of the jobs executes a different command. +Of course a command can execute code directly (`./configure;make;make install`) or run a script (`test.sh`) in the repository. + +Jobs are used to create builds, which are then picked up by [runners](../runners/README.md) and executed within the environment of the runner. +What is important, is that each job is run independently from each other. + +## .gitlab-ci.yml +The YAML syntax allows for using more complex job specifications than in the above example: + +```yaml +image: ruby:2.1 +services: + - postgres + +before_script: + - bundle_install + +stages: + - build + - test + - deploy + +job1: + stage: build + script: + - execute-script-for-job1 + only: + - master + tags: + - docker +``` + +There are a few `keywords` that can't be used as job names: + +| keyword | required | description | +|---------------|----------|-------------| +| image | optional | Use docker image, covered in [Use Docker](../docker/README.md) | +| services | optional | Use docker services, covered in [Use Docker](../docker/README.md) | +| stages | optional | Define build stages | +| types | optional | Alias for `stages` | +| before_script | optional | Define commands prepended for each job's script | +| variables | optional | Define build variables | + +### image and services +This allows to specify a custom Docker image and a list of services that can be used for time of the build. +The configuration of this feature is covered in separate document: [Use Docker](../docker/README.md). + +### before_script +`before_script` is used to define the command that should be run before all builds, including deploy builds. This can be an array or a multiline string. + +### stages +`stages` is used to define build stages that can be used by jobs. +The specification of `stages` allows for having flexible multi stage pipelines. + +The ordering of elements in `stages` defines the ordering of builds' execution: + +1. Builds of the same stage are run in parallel. +1. Builds of next stage are run after success. + +Let's consider the following example, which defines 3 stages: +``` +stages: + - build + - test + - deploy +``` + +1. First all jobs of `build` are executed in parallel. +1. If all jobs of `build` succeeds, the `test` jobs are executed in parallel. +1. If all jobs of `test` succeeds, the `deploy` jobs are executed in parallel. +1. If all jobs of `deploy` succeeds, the commit is marked as `success`. +1. If any of the previous jobs fails, the commit is marked as `failed` and no jobs of further stage are executed. + +There are also two edge cases worth mentioning: + +1. If no `stages` is defined in `.gitlab-ci.yml`, then by default the `build`, `test` and `deploy` are allowed to be used as job's stage by default. +2. If a job doesn't specify `stage`, the job is assigned the `test` stage. + +### types +Alias for [stages](#stages). + +### variables +**This feature requires `gitlab-runner` with version equal or greater than 0.5.0.** + +GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build environment. +The variables are stored in repository and are meant to store non-sensitive project configuration, ie. RAILS_ENV or DATABASE_URL. + +```yaml +variables: + DATABASE_URL: "postgres://postgres@postgres/my_database" +``` + +These variables can be later used in all executed commands and scripts. + +The YAML-defined variables are also set to all created service containers, thus allowing to fine tune them. + +## Jobs +`.gitlab-ci.yml` allows you to specify an unlimited number of jobs. +Each job has to have a unique `job_name`, which is not one of the keywords mentioned above. +A job is defined by a list of parameters that define the build behaviour. + +```yaml +job_name: + script: + - rake spec + - coverage + stage: test + only: + - master + except: + - develop + tags: + - ruby + - postgres + allow_failure: true +``` + +| keyword | required | description | +|---------------|----------|-------------| +| script | required | Defines a shell script which is executed by runner | +| stage | optional (default: test) | Defines a build stage | +| type | optional | Alias for `stage` | +| only | optional | Defines a list of git refs for which build is created | +| except | optional | Defines a list of git refs for which build is not created | +| tags | optional | Defines a list of tags which are used to select runner | +| allow_failure | optional | Allow build to fail. Failed build doesn't contribute to commit status | + +### script +`script` is a shell script which is executed by runner. The shell script is prepended with `before_script`. + +```yaml +job: + script: "bundle exec rspec" +``` + +This parameter can also contain several commands using an array: +```yaml +job: + script: + - uname -a + - bundle exec rspec +``` + +### stage +`stage` allows to group build into different stages. Builds of the same `stage` are executed in `parallel`. +For more info about the use of `stage` please check the [stages](#stages). + +### only and except +This are two parameters that allow for setting a refs policy to limit when jobs are built: +1. `only` defines the names of branches and tags for which job will be built. +2. `except` defines the names of branches and tags for which the job wil **not** be built. + +There are a few rules that apply to usage of refs policy: + +1. `only` and `except` are exclusive. If both `only` and `except` are defined in job specification only `only` is taken into account. +1. `only` and `except` allow for using the regexp expressions. +1. `only` and `except` allow for using special keywords: `branches` and `tags`. +These names can be used for example to exclude all tags and all branches. + +```yaml +job: + only: + - /^issue-.*$/ # use regexp + except: + - branches # use special keyword +``` + +### tags +`tags` is used to select specific runners from the list of all runners that are allowed to run this project. + +During registration of a runner, you can specify the runner's tags, ie.: `ruby`, `postgres`, `development`. +`tags` allow you to run builds with runners that have the specified tags assigned: + +``` +job: + tags: + - ruby + - postgres +``` + +The above specification will make sure that `job` is built by a runner that have `ruby` AND `postgres` tags defined. + +## Validate the .gitlab-ci.yml +Each instance of GitLab CI has an embedded debug tool called Lint. +You can find the link to the Lint in the project's settings page or use short url `/lint`. + +## Skipping builds +There is one more way to skip all builds, if your commit message contains tag [ci skip]. In this case, commit will be created but builds will be skipped
\ No newline at end of file diff --git a/doc/gitlab-basics/create-your-ssh-keys.md b/doc/gitlab-basics/create-your-ssh-keys.md index c8a73feb028..f31c353f2cf 100644 --- a/doc/gitlab-basics/create-your-ssh-keys.md +++ b/doc/gitlab-basics/create-your-ssh-keys.md @@ -10,11 +10,7 @@ After you confirm, go to GitLab and sign in to your account. ## Add your SSH Key -At the top right corner, click on "profile settings": - - - -On the left side menu click on "SSH Keys": +On the left side menu, click on "profile settings" and then click on "SSH Keys":  diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md index f7d4f3de68b..548c484bc08 100644 --- a/doc/hooks/custom_hooks.md +++ b/doc/hooks/custom_hooks.md @@ -2,7 +2,7 @@ **Note: Custom git hooks must be configured on the filesystem of the GitLab server. Only GitLab server administrators will be able to complete these tasks. -Please explore webhooks as an option if you do not have filesystem access.** +Please explore webhooks as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).** Git natively supports hooks that are executed on different actions. Examples of server-side git hooks include pre-receive, post-receive, and update. diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md new file mode 100644 index 00000000000..01ab22321ed --- /dev/null +++ b/doc/incoming_email/README.md @@ -0,0 +1,207 @@ +# Reply by email + +GitLab can be set up to allow users to comment on issues and merge requests by replying to notification emails. + +## Get a mailbox + +Reply by email requires an IMAP-enabled email account, with a provider or server that supports [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing). Sub-addressing is a feature where any email to `user+some_arbitrary_tag@example.com` will end up in the mailbox for `user@example.com`, and is supported by providers such as Gmail, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix mail server which you can run on-premises. + +If you want to use Gmail with Reply by email, make sure you have [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) and [allow less secure apps to access the account](https://support.google.com/accounts/answer/6010255). + +To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these instructions](./postfix.md). + +## Set it up + +In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`. + +### Omnibus package installations + +1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the feature, enter the email address including a placeholder for the `key` that references the item being replied to and fill in the details for your specific IMAP server and email account: + + ```ruby + gitlab_rails['incoming_email_enabled'] = true + gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com" + gitlab_rails['incoming_email_host'] = "imap.gmail.com" # IMAP server host + gitlab_rails['incoming_email_port'] = 993 # IMAP server port + gitlab_rails['incoming_email_ssl'] = true # Whether the IMAP server uses SSL + gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com" # Email account username. Usually the full email address. + gitlab_rails['incoming_email_password'] = "password" # Email account password + gitlab_rails['incoming_email_mailbox_name'] = "inbox" # The name of the mailbox where incoming mail will end up. Usually "inbox". + ``` + + As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `gitlab-incoming@gmail.com`. + +1. Reconfigure GitLab for the changes to take effect: + + ```sh + sudo gitlab-ctl reconfigure + ``` + +1. Verify that everything is configured correctly: + + ```sh + sudo gitlab-rake gitlab:incoming_email:check + ``` + +1. Reply by email should now be working. + +### Installations from source + +1. Go to the GitLab installation directory: + + ```sh + cd /home/git/gitlab + ``` + +1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and enter the email address including a placeholder for the `key` that references the item being replied to: + + ```sh + sudo editor config/gitlab.yml + ``` + + ```yaml + incoming_email: + enabled: true + address: "gitlab-incoming+%{key}@gmail.com" + ``` + + As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `gitlab-incoming@gmail.com`. + +2. Copy `config/mail_room.yml.example` to `config/mail_room.yml`: + + ```sh + sudo cp config/mail_room.yml.example config/mail_room.yml + ``` + +3. Uncomment the configuration options in `config/mail_room.yml` and fill in the details for your specific IMAP server and email account: + + ```sh + sudo editor config/mail_room.yml + ``` + + ```yaml + :mailboxes: + - + # IMAP server host + :host: "imap.gmail.com" + # IMAP server port + :port: 993 + # Whether the IMAP server uses SSL + :ssl: true + # Whether the IMAP server uses StartTLS + :start_tls: false + # Email account username. Usually the full email address. + :email: "gitlab-incoming@gmail.com" + # Email account password + :password: "[REDACTED]" + # The name of the mailbox where incoming mail will end up. Usually "inbox". + :name: "inbox" + # Always "sidekiq". + :delivery_method: sidekiq + # Always true. + :delete_after_delivery: true + :delivery_options: + # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml. + :redis_url: redis://localhost:6379 + # Always "resque:gitlab". + :namespace: resque:gitlab + # Always "incoming_email". + :queue: incoming_email + # Always "EmailReceiverWorker" + :worker: EmailReceiverWorker + ``` + +5. Edit the init script configuration at `/etc/default/gitlab` to enable `mail_room`: + + ```sh + sudo mkdir -p /etc/default + echo 'mail_room_enabled=true' | sudo tee -a /etc/default/gitlab + ``` + +6. Restart GitLab: + + ```sh + sudo service gitlab restart + ``` + +7. Verify that everything is configured correctly: + + ```sh + sudo -u git -H bundle exec rake gitlab:incoming_email:check RAILS_ENV=production + ``` + +8. Reply by email should now be working. + +### Development + +1. Go to the GitLab installation directory. + +1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and enter the email address including a placeholder for the `key` that references the item being replied to: + + ```yaml + incoming_email: + enabled: true + address: "gitlab-incoming+%{key}@gmail.com" + ``` + + As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`. + +2. Copy `config/mail_room.yml.example` to `config/mail_room.yml`: + + ```sh + sudo cp config/mail_room.yml.example config/mail_room.yml + ``` + +3. Uncomment the configuration options in `config/mail_room.yml` and fill in the details for your specific IMAP server and email account: + + ```yaml + :mailboxes: + - + # IMAP server host + :host: "imap.gmail.com" + # IMAP server port + :port: 993 + # Whether the IMAP server uses SSL + :ssl: true + # Whether the IMAP server uses StartTLS + :start_tls: false + # Email account username. Usually the full email address. + :email: "gitlab-incoming@gmail.com" + # Email account password + :password: "[REDACTED]" + # The name of the mailbox where incoming mail will end up. Usually "inbox". + :name: "inbox" + # Always "sidekiq". + :delivery_method: sidekiq + # Always true. + :delete_after_delivery: true + :delivery_options: + # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml. + :redis_url: redis://localhost:6379 + # Always "resque:gitlab". + :namespace: resque:gitlab + # Always "incoming_email". + :queue: incoming_email + # Always "EmailReceiverWorker" + :worker: EmailReceiverWorker + ``` + +4. Uncomment the `mail_room` line in your `Procfile`: + + ```yaml + mail_room: bundle exec mail_room -q -c config/mail_room.yml + ``` + +6. Restart GitLab: + + ```sh + bundle exec foreman start + ``` + +7. Verify that everything is configured correctly: + + ```sh + bundle exec rake gitlab:incoming_email:check RAILS_ENV=development + ``` + +8. Reply by email should now be working. diff --git a/doc/incoming_email/postfix.md b/doc/incoming_email/postfix.md new file mode 100644 index 00000000000..18bf3db1744 --- /dev/null +++ b/doc/incoming_email/postfix.md @@ -0,0 +1,310 @@ +# Set up Postfix for Reply by email + +This document will take you through the steps of setting up a basic Postfix mail server with IMAP authentication on Ubuntu, to be used with Reply by email. + +The instructions make the assumption that you will be using the email address `incoming@gitlab.example.com`, that is, username `incoming` on host `gitlab.example.com`. Don't forget to change it to your actual host when executing the example code snippets. + +## Configure your server firewall + +1. Open up port 25 on your server so that people can send email into the server over SMTP. +2. If the mail server is different from the server running GitLab, open up port 143 on your server so that GitLab can read email from the server over IMAP. + +## Install packages + +1. Install the `postfix` package if it is not installed already: + + ```sh + sudo apt-get install postfix + ``` + + When asked about the environment, select 'Internet Site'. When asked to confirm the hostname, make sure it matches `gitlab.example.com`. + +1. Install the `mailutils` package. + + ```sh + sudo apt-get install mailutils + ``` + +## Create user + +1. Create a user for incoming email. + + ```sh + sudo useradd -m -s /bin/bash incoming + ``` + +1. Set a password for this user. + + ```sh + sudo passwd incoming + ``` + + Be sure not to forget this, you'll need it later. + +## Test the out-of-the-box setup + +1. Connect to the local SMTP server: + + ```sh + telnet localhost 25 + ``` + + You should see a prompt like this: + + ```sh + Trying 127.0.0.1... + Connected to localhost. + Escape character is '^]'. + 220 gitlab.example.com ESMTP Postfix (Ubuntu) + ``` + + If you get a `Connection refused` error instead, verify that `postfix` is running: + + ```sh + sudo postfix status + ``` + + If it is not, start it: + + ```sh + sudo postfix start + ``` + +1. Send the new `incoming` user a dummy email to test SMTP, by entering the following into the SMTP prompt: + + ``` + ehlo localhost + mail from: root@localhost + rcpt to: incoming@localhost + data + Subject: Re: Some issue + + Sounds good! + . + quit + ``` + + (Note: The `.` is a literal period on its own line) + +1. Check if the `incoming` user received the email: + + ```sh + su - incoming + mail + ``` + + You should see output like this: + + ``` + "/var/mail/incoming": 1 message 1 unread + >U 1 root@localhost 59/2842 Re: Some issue + ``` + + Quit the mail app: + + ```sh + q + ``` + +1. Log out of the `incoming` account and go back to being `root`: + + ```sh + logout + ``` + +## Configure Postfix to use Maildir-style mailboxes + +Courier, which we will install later to add IMAP authentication, requires mailboxes to have the Maildir format, rather than mbox. + +1. Configure Postfix to use Maildir-style mailboxes: + + ```sh + sudo postconf -e "home_mailbox = Maildir/" + ``` + +1. Restart Postfix: + + ```sh + sudo /etc/init.d/postfix restart + ``` + +1. Test the new setup: + + 1. Follow steps 1 and 2 of _[Test the out-of-the-box setup](#test-the-out-of-the-box-setup)_. + 2. Check if the `incoming` user received the email: + + ```sh + su - incoming + MAIL=/home/incoming/Maildir + mail + ``` + + You should see output like this: + + ``` + "/home/incoming/Maildir": 1 message 1 unread + >U 1 root@localhost 59/2842 Re: Some issue + ``` + + Quit the mail app: + + ```sh + q + ``` + +1. Log out of the `incoming` account and go back to being `root`: + + ```sh + logout + ``` + +## Install the Courier IMAP server + +1. Install the `courier-imap` package: + + ```sh + sudo apt-get install courier-imap + ``` + +## Configure Postfix to receive email from the internet + +1. Let Postfix know about the domains that it should consider local: + + ```sh + sudo postconf -e "mydestination = gitlab.example.com, localhost.localdomain, localhost" + ``` + +1. Let Postfix know about the IPs that it should consider part of the LAN: + + We'll assume `192.168.1.0/24` is your local LAN. You can safely skip this step if you don't have other machines in the same local network. + + ```sh + sudo postconf -e "mynetworks = 127.0.0.0/8, 192.168.1.0/24" + ``` + +1. Configure Postfix to receive mail on all interfaces, which includes the internet: + + ```sh + sudo postconf -e "inet_interfaces = all" + ``` + +1. Configure Postfix to use the `+` delimiter for sub-addressing: + + ```sh + sudo postconf -e "recipient_delimiter = +" + ``` + +1. Restart Postfix: + + ```sh + sudo service postfix restart + ``` + +## Test the final setup + +1. Test SMTP under the new setup: + + 1. Connect to the SMTP server: + + ```sh + telnet gitlab.example.com 25 + ``` + + You should see a prompt like this: + + ```sh + Trying 123.123.123.123... + Connected to gitlab.example.com. + Escape character is '^]'. + 220 gitlab.example.com ESMTP Postfix (Ubuntu) + ``` + + If you get a `Connection refused` error instead, make sure your firewall is setup to allow inbound traffic on port 25. + + 1. Send the `incoming` user a dummy email to test SMTP, by entering the following into the SMTP prompt: + + ``` + ehlo gitlab.example.com + mail from: root@gitlab.example.com + rcpt to: incoming@gitlab.example.com + data + Subject: Re: Some issue + + Sounds good! + . + quit + ``` + + (Note: The `.` is a literal period on its own line) + + 1. Check if the `incoming` user received the email: + + ```sh + su - incoming + MAIL=/home/incoming/Maildir + mail + ``` + + You should see output like this: + + ``` + "/home/incoming/Maildir": 1 message 1 unread + >U 1 root@gitlab.example.com 59/2842 Re: Some issue + ``` + + Quit the mail app: + + ```sh + q + ``` + + 1. Log out of the `incoming` account and go back to being `root`: + + ```sh + logout + ``` + +1. Test IMAP under the new setup: + + 1. Connect to the IMAP server: + + ```sh + telnet gitlab.example.com 143 + ``` + + You should see a prompt like this: + + ```sh + Trying 123.123.123.123... + Connected to mail.example.gitlab.com. + Escape character is '^]'. + - OK [CAPABILITY IMAP4rev1 UIDPLUS CHILDREN NAMESPACE THREAD=ORDEREDSUBJECT THREAD=REFERENCES SORT QUOTA IDLE ACL ACL2=UNION] Courier-IMAP ready. Copyright 1998-2011 Double Precision, Inc. See COPYING for distribution information. + ``` + + 1. Sign in as the `incoming` user to test IMAP, by entering the following into the IMAP prompt: + + ``` + a login incoming PASSWORD + ``` + + Replace PASSWORD with the password you set on the `incoming` user earlier. + + You should see output like this: + + ``` + a OK LOGIN Ok. + ``` + + 1. Disconnect from the IMAP server: + + ```sh + a logout + ``` + +## Done! + +If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./README.md) guide to configure GitLab. + +--------- + +_This document was adapted from https://help.ubuntu.com/community/PostfixBasicSetupHowto, by contributors to the Ubuntu documentation wiki._ diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md index 362c492d0ac..c565e90da2f 100644 --- a/doc/install/database_mysql.md +++ b/doc/install/database_mysql.md @@ -36,7 +36,7 @@ We do not recommend using MySQL due to various issues. For example, case [(in)se mysql> CREATE DATABASE IF NOT EXISTS `gitlabhq_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`; # Grant the GitLab user necessary permissions on the database - mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, LOCK TABLES ON `gitlabhq_production`.* TO 'git'@'localhost'; + mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER, LOCK TABLES ON `gitlabhq_production`.* TO 'git'@'localhost'; # Quit the database session mysql> \q diff --git a/doc/install/installation.md b/doc/install/installation.md index 52031e9b9a1..518b914fe67 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -115,8 +115,9 @@ Remove the old Ruby 1.8 if present Download Ruby and compile it: mkdir /tmp/ruby && cd /tmp/ruby - curl -L --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.6.tar.gz | tar xz - cd ruby-2.1.6 + curl -O --progress https://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.7.tar.gz + echo 'e2e195a4a58133e3ad33b955c829bb536fa3c075 ruby-2.1.7.tar.gz' | shasum -c - && tar xzf ruby-2.1.7.tar.gz + cd ruby-2.1.7 ./configure --disable-install-rdoc make sudo make install @@ -131,11 +132,11 @@ Since GitLab 8.0, Git HTTP requests are handled by gitlab-git-http-server. This is a small daemon written in Go. To install gitlab-git-http-server we need a Go compiler. - curl -O --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz - echo '5817fa4b2252afdb02e11e8b9dc1d9173ef3bd5a go1.5.linux-amd64.tar.gz' | shasum -c - && \ - sudo tar -C /usr/local -xzf go1.5.linux-amd64.tar.gz + curl -O --progress https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz + echo '46eecd290d8803887dec718c691cc243f2175fe0 go1.5.1.linux-amd64.tar.gz' | shasum -c - && \ + sudo tar -C /usr/local -xzf go1.5.1.linux-amd64.tar.gz sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ - rm go1.5.linux-amd64.tar.gz + rm go1.5.1.linux-amd64.tar.gz ## 4. System Users @@ -207,9 +208,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-14-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-0-stable gitlab -**Note:** You can change `7-14-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -222,6 +223,10 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da # Update GitLab config file, follow the directions at top of file sudo -u git -H editor config/gitlab.yml + # Copy the example secrets file + sudo -u git -H cp config/secrets.yml.example config/secrets.yml + sudo -u git -H chmod 0600 config/secrets.yml + # Make sure GitLab can write to the log/ and tmp/ directories sudo chown -R git log/ sudo chown -R git tmp/ @@ -235,6 +240,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da # Make sure GitLab can write to the public/uploads/ directory sudo chmod -R u+rwX public/uploads + # Change the permissions of the directory where CI build traces are stored + sudo chmod -R u+rwX builds/ + # Copy the example Unicorn config sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb @@ -299,7 +307,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da GitLab Shell is an SSH access and repository management software developed specially for GitLab. # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): - sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.3] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production + sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.5] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production # By default, the gitlab-shell config is generated from your main GitLab config. # You can review (and modify) the gitlab-shell config as follows: @@ -328,6 +336,17 @@ GitLab Shell is an SSH access and repository management software developed speci sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword +### Secure secrets.yml + +The `secrets.yml` file stores encryption keys for sessions and secure variables. +Backup `secrets.yml` someplace safe, but don't store it in the same place as your database backups. +Otherwise your secrets are exposed if one of your backups is compromised. + +### Install schedules + + # Setup schedules + sudo -u gitlab_ci -H bundle exec whenever -w RAILS_ENV=production + ### Install Init Script Download the init script (will be `/etc/init.d/gitlab`): @@ -456,9 +475,22 @@ Using a self-signed certificate is discouraged but if you must use it follow the ``` 1. In the `config.yml` of gitlab-shell set `self_signed_cert` to `true`. -### Additional Markup Styles +### Enable Reply by email -Apart from the always supported markdown style there are other rich text files that GitLab can display. But you might have to install a dependency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information. +See the ["Reply by email" documentation](../incoming_email/README.md) for more information on how to set this up. + +### LDAP Authentication + +You can configure LDAP authentication in `config/gitlab.yml`. Please restart GitLab after editing this file. + +### Using Custom Omniauth Providers + +See the [omniauth integration document](../integration/omniauth.md) + +### Build your projects + +GitLab can build your projects. To enable that feature you need GitLab Runners to do that for you. +Checkout the [Gitlab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-runner) to install it ### Custom Redis Connection @@ -484,10 +516,16 @@ If you are running SSH on a non-standard port, you must change the GitLab user's You also need to change the corresponding options (e.g. `ssh_user`, `ssh_host`, `admin_uri`) in the `config\gitlab.yml` file. -### LDAP Authentication +### Additional Markup Styles -You can configure LDAP authentication in `config/gitlab.yml`. Please restart GitLab after editing this file. +Apart from the always supported markdown style there are other rich text files that GitLab can display. But you might have to install a dependency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information. -### Using Custom Omniauth Providers +## Troubleshooting -See the [omniauth integration document](../integration/omniauth.md) +### "You appear to have cloned an empty repository." + +If you see this message when attempting to clone a repository hosted by GitLab, +this is likely due to an outdated Nginx or Apache configuration, or a missing or +misconfigured `gitlab-git-http-server` instance. Double-check that you've +[installed Go](#3-go), [installed gitlab-git-http-server](#install-gitlab-git-http-server), +and correctly [configured Nginx](#site-configuration). diff --git a/doc/integration/README.md b/doc/integration/README.md index 6d856951d4e..eff39a626ae 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -10,7 +10,7 @@ See the documentation below for details on how to configure these services. - [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider - [Slack](slack.md) Integrate with the Slack chat service - [OAuth2 provider](oauth_provider.md) OAuth2 application creation -- [Gmail](gitlab_buttons_in_gmail.md) Adds GitLab actions to messages +- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages GitLab Enterprise Edition contains [advanced JIRA support](http://doc.gitlab.com/ee/integration/jira.html) and [advanced Jenkins support](http://doc.gitlab.com/ee/integration/jenkins.html). diff --git a/doc/integration/gmail_action_buttons_for_gitlab.md b/doc/integration/gmail_action_buttons_for_gitlab.md new file mode 100644 index 00000000000..de45f25ad62 --- /dev/null +++ b/doc/integration/gmail_action_buttons_for_gitlab.md @@ -0,0 +1,22 @@ +# Gmail actions buttons for GitLab + +GitLab supports [Google actions in email](https://developers.google.com/gmail/markup/actions/actions-overview). + +If correctly setup, emails that require an action will be marked in Gmail. + + + +To get this functioning, you need to be registered with Google. +[See how to register with Google in this document.](https://developers.google.com/gmail/markup/registering-with-google) + +*This process has a lot of steps so make sure that you fulfill all requirements set by Google.* +*Your application will be rejected by Google if you fail to do so.* + +Pay close attention to: + +* Email account used by GitLab to send notification emails needs to have "Consistent history of sending a high volume of mail from your domain (order of hundred emails a day minimum to Gmail) for a few weeks at least". +* "A very very low rate of spam complaints from users." +* Emails must be authenticated via DKIM or SPF +* Before sending the final form("Gmail Schema Whitelist Request"), you must send a real email from your production server. This means that you will have to find a way to send this email from the email address you are registering. You can do this by, for example, forwarding the real email from the email address you are registering or going into the rails console on the GitLab server and triggering the email sending from there. + +You can check how it looks going through all the steps laid out in the "Registering with Google" doc in [this GitLab.com issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/1517). diff --git a/doc/integration/gmail_actions_button.png b/doc/integration/gmail_actions_button.png Binary files differnew file mode 100644 index 00000000000..b08f54d137b --- /dev/null +++ b/doc/integration/gmail_actions_button.png diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index 904d5d7fee2..9b7d8fa3969 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -78,6 +78,26 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server # user_filter: '' + # LDAP attributes that GitLab will use to create an account for the LDAP user. + # The specified attribute can either be the attribute name as a string (e.g. 'mail'), + # or an array of attribute names to try in order (e.g. ['mail', 'email']). + # Note that the user's LDAP login will always be the attribute specified as `uid` above. + attributes: + # The username will be used in paths for the user's own projects + # (like `gitlab.example.com/username/project`) and when mentioning + # them in issues, merge request and comments (like `@username`). + # If the attribute specified for `username` contains an email address, + # the GitLab username will be the part of the email address before the '@'. + username: ['uid', 'userid', 'sAMAccountName'] + email: ['mail', 'email', 'userPrincipalName'] + + # If no full name could be found at the attribute specified for `name`, + # the full name is determined using the attributes specified for + # `first_name` and `last_name`. + name: 'cn' + first_name: 'givenName' + last_name: 'sn' + # GitLab EE only: add more LDAP servers # Choose an ID made of a-z and 0-9 . This ID will be stored in the database # so that GitLab can remember which LDAP server a user belongs to. @@ -153,3 +173,23 @@ Tip: if you want to limit access to the nested members of an Active Directory gr ``` Please note that GitLab does not support the custom filter syntax used by omniauth-ldap. + +## Limitations + +GitLab's LDAP client is based on [omniauth-ldap](https://gitlab.com/gitlab-org/omniauth-ldap) +which encapsulates Ruby's `Net::LDAP` class. It provides a pure-Ruby implementation +of the LDAP client protocol. As a result, GitLab is limited by `omniauth-ldap` and may impact your LDAP +server settings. + +### TLS Client Authentication +Not implemented by `Net::LDAP`. +So you should disable anonymous LDAP authentication and enable simple or SASL +authentication. TLS client authentication setting in your LDAP server cannot be +mandatory and clients cannot be authenticated with the TLS protocol. + +### TLS Server Authentication +Not supported by GitLab's configuration options. +When setting `method: ssl`, the underlying authentication method used by +`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with +the LDAP server before any LDAP-protocol data is exchanged but no validation of +the LDAP server's SSL certificate is performed.
\ No newline at end of file diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index 322111ae9e1..ac3851f8c95 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -33,7 +33,6 @@ For GitLab we developed something we call "GitLab Flavored Markdown" (GFM). It e You can use GFM in -- commit messages - comments - issues - merge requests @@ -275,7 +274,7 @@ The IDs are generated from the content of the header according to the following 1. All spaces are converted to hyphens 1. Two or more hyphens in a row are converted to one 1. If a header with the same ID has already been generated, a unique - incrementing number is appended. + incrementing number is appended, starting at 1. For example: @@ -292,8 +291,8 @@ Would generate the following link IDs: 1. `this-header-has-spaces-in-it` 1. `this-header-has-a-in-it` 1. `this-header-has-unicode-in-it-한글` +1. `this-header-has-spaces-in-it` 1. `this-header-has-spaces-in-it-1` -1. `this-header-has-spaces-in-it-2` Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID. @@ -413,7 +412,7 @@ Some text to show that the reference links can follow later. Relative links do not allow referencing project files in a wiki page or wiki page in a project file. The reason for this is that, in GitLab, wiki is always a separate git repository. For example: -`[I'm a reference-style link][style]` +`[I'm a reference-style link](style)` will point the link to `wikis/style` when the link is inside of a wiki markdown file. diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md new file mode 100644 index 00000000000..56bf7a14182 --- /dev/null +++ b/doc/migrate_ci_to_ce/README.md @@ -0,0 +1,321 @@ +## Migrate GitLab CI to GitLab CE or EE + +Beginning with version 8.0 of GitLab Community Edition (CE) and Enterprise +Edition (EE), GitLab CI is no longer its own application, but is instead built +into the CE and EE applications. + +This guide will detail the process of migrating your CI installation and data +into your GitLab CE or EE installation. **You can only migrate CI data from +GitLab CI 8.0 to GitLab 8.0; migrating between other versions (e.g.7.14 to 8.1) +is not possible.** + +We recommend that you read through the entire migration process in this +document before beginning. + +### Overview + +In this document we assume you have a GitLab server and a GitLab CI server. It +does not matter if these are the same machine. + +The migration consists of three parts: updating GitLab and GitLab CI, moving +data, and redirecting traffic. + +Please note that CI builds triggered on your GitLab server in the time between +updating to 8.0 and finishing the migration will be lost. Your GitLab server +can be online for most of the procedure; the only GitLab downtime (if any) is +during the upgrade to 8.0. Your CI service will be offline from the moment you +upgrade to 8.0 until you finish the migration procedure. + +### Before upgrading + +If you have GitLab CI installed using omnibus-gitlab packages but *you don't want to migrate your existing data*: + +```bash +mv /var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/gitlab-ci/builds.$(date +%s) +``` + +and run `sudo gitlab-ctl reconfigure`. + +#### 1. Verify that backups work + +Make sure that the backup script on both servers can connect to the database. + +``` +# On your CI server: +# Omnibus +sudo gitlab-ci-rake backup:create + +# Source +cd /home/gitlab_ci/gitlab-ci +sudo -u gitlab_ci -H bundle exec rake backup:create RAILS_ENV=production +``` + +Also check on your GitLab server. + +``` +# On your GitLab server: +# Omnibus +sudo gitlab-rake gitlab:backup:create SKIP=repositories,uploads + +# Source +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production SKIP=repositories,uploads +``` + +If this fails you need to fix it before upgrading to 8.0. Also see +https://about.gitlab.com/getting-help/ + +#### 2. Check source and target database types + +Check what databases you use on your GitLab server and your CI server. + Look for the 'adapter:' line. If your CI server and your GitLab server use +the same database adapter no special care is needed. If your CI server uses +MySQL and your GitLab server uses PostgreSQL you need to pass a special option +during the 'Moving data' part. **If your CI server uses PostgreSQL and your +GitLab server uses MySQL you cannot migrate your CI data to GitLab 8.0.** + +``` +# On your CI server: +# Omnibus +sudo gitlab-ci-rake env:info + +# Source +cd /home/gitlab_ci/gitlab-ci +sudo -u gitlab_ci -H bundle exec rake env:info RAILS_ENV=production +``` + +``` +# On your GitLab server: +# Omnibus +sudo gitlab-rake gitlab:env:info + +# Source +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +#### 3. Storage planning + +Decide where to store CI build traces on GitLab server. GitLab CI uses + files on disk to store CI build traces. The default path for these build +traces is `/var/opt/gitlab/gitlab-ci/builds` (Omnibus) or +`/home/git/gitlab/builds` (Source). If you are storing your repository data in +a special location, or if you are using NFS, you should make sure that you +store build traces on the same storage as your Git repositories. + +### I. Upgrading + +From this point on, GitLab CI will be unavailable for your end users. + +#### 1. Upgrade GitLab to 8.0 + +First upgrade your GitLab server to version 8.0: +https://about.gitlab.com/update/ + +#### 2. Disable CI on the GitLab server during the migration + +After you update, go to the admin panel and temporarily disable CI. As + an administrator, go to **Admin Area** -> **Settings**, and under +**Continuous Integration** uncheck **Disable to prevent CI usage until rake +ci:migrate is run (8.0 only)**. + +#### 3. CI settings are now in GitLab + +If you want to use custom CI settings (e.g. change where builds are + stored), please update `/etc/gitlab/gitlab.rb` (Omnibus) or +`/home/git/gitlab/config/gitlab.yml` (Source). + +#### 4. Upgrade GitLab CI to 8.0 + +Now upgrade GitLab CI to version 8.0. If you are using Omnibus packages, + this may have already happened when you upgraded GitLab to 8.0. + +#### 5. Disable GitLab CI on the CI server + +Disable GitLab CI after upgrading to 8.0. + +``` +# On your CI server: +# Omnibus +sudo gitlab-ctl stop ci-unicorn +sudo gitlab-ctl stop ci-sidekiq + +# Source +sudo service gitlab_ci stop +cd /home/gitlab_ci/gitlab-ci +sudo -u gitlab_ci -H bundle exec whenever --clear-crontab RAILS_ENV=production +``` + +### II. Moving data + +#### 1. Database encryption key + +Move the database encryption key from your CI server to your GitLab + server. The command below will show you what you need to copy-paste to your +GitLab server. On Omnibus GitLab servers you will have to add a line to +`/etc/gitlab/gitlab.rb`. On GitLab servers installed from source you will have +to replace the contents of `/home/git/gitlab/config/secrets.yml`. + +``` +# On your CI server: +# Omnibus +sudo gitlab-ci-rake backup:show_secrets + +# Source +cd /home/gitlab_ci/gitlab-ci +sudo -u gitlab_ci -H bundle exec rake backup:show_secrets RAILS_ENV=production +``` + +#### 2. SQL data and build traces + +Create your final CI data export. If you are converting from MySQL to + PostgreSQL, add ` MYSQL_TO_POSTGRESQL=1` to the end of the rake command. When +the command finishes it will print the path to your data export archive; you +will need this file later. + +``` +# On your CI server: +# Omnibus +sudo gitlab-ci-rake backup:create + +# Source +cd /home/gitlab_ci/gitlab-ci +sudo -u gitlab_ci -H bundle exec rake backup:create RAILS_ENV=production +``` + +#### 3. Copy data to the GitLab server + +If you were running GitLab and GitLab CI on the same server you can skip this +step. + +Copy your CI data archive to your GitLab server. There are many ways to do +this, below we use SSH agent forwarding and 'scp', which will be easy and fast +for most setups. You can also copy the data archive first from the CI server to +your laptop and then from your laptop to the GitLab server. + +``` +# Start from your laptop +ssh -A ci_admin@ci_server.example +# Now on the CI server +scp /path/to/12345_gitlab_ci_backup.tar gitlab_admin@gitlab_server.example:~ +``` + +#### 4. Move data to the GitLab backups folder + +Make the CI data archive discoverable for GitLab. We assume below that you +store backups in the default path, adjust the command if necessary. + +``` +# On your GitLab server: +# Omnibus +sudo mv /path/to/12345_gitlab_ci_backup.tar /var/opt/gitlab/backups/ + +# Source +sudo mv /path/to/12345_gitlab_ci_backup.tar /home/git/gitlab/tmp/backups/ +``` + +#### 5. Import the CI data into GitLab. + +This step will delete any existing CI data on your GitLab server. There should +be no CI data yet because you turned CI on the GitLab server off earlier. + +``` +# On your GitLab server: +# Omnibus +sudo gitlab-rake ci:migrate + +# Source +cd /home/git/gitlab +sudo -u git -H bundle exec rake ci:migrate RAILS_ENV=production +``` + +#### 6. Restart GitLab + +``` +# On your GitLab server: +# Omnibus +sudo gitlab-ctl hup unicorn +sudo gitlab-ctl restart sidekiq + +# Source +sudo service gitlab reload +``` + +### III. Redirecting traffic + +If you were running GitLab CI with Omnibus packages and you were using the +internal NGINX configuration your CI service should now be available both at +`ci.example.com` (the old address) and `gitlab.example.com/ci`. **You are done!** + +If you installed GitLab CI from source we now need to configure a redirect in +NGINX so that existing CI runners can keep using the old CI server address, and +so that existing links to your CI server keep working. + +#### 1. Update Nginx configuration + +To ensure that your existing CI runners are able to communicate with the +migrated installation, and that existing build triggers still work, you'll need +to update your Nginx configuration to redirect requests for the old locations to +the new ones. + +Edit `/etc/nginx/sites-available/gitlab_ci` and paste: + +```nginx +# 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; +} +``` + +Make sure you substitute these placeholder values with your real ones: + +1. `YOUR_CI_SERVER_FQDN`: The existing public-facing address of your GitLab CI + install (e.g., `ci.gitlab.com`). +1. `YOUR_GITLAB_SERVER_FQDN`: The current public-facing address of your GitLab + CE (or EE) install (e.g., `gitlab.com`). + +**Make sure not to remove the `/ci$request_uri` part. This is required to +properly forward the requests.** + +You should also make sure that you can: + +1. `curl https://YOUR_GITLAB_SERVER_FQDN/` from your previous GitLab CI server. +1. `curl https://YOUR_CI_SERVER_FQDN/` from your GitLab CE (or EE) server. + +#### 2. Check Nginx configuration + + sudo nginx -t + +#### 3. Restart Nginx + + sudo /etc/init.d/nginx restart + +#### Restore from backup + +If something went wrong and you need to restore a backup, consult the [Backup +restoration](../raketasks/backup_restore.md) guide. diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index ad1914398d6..db3f6bb40bd 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -97,6 +97,8 @@ For installations from source: aws_secret_access_key: 'secret123' # The remote 'directory' to store your backups. For S3, this would be the bucket name. remote_directory: 'my.s3.bucket' + # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional + # encryption: 'AES256' ``` If you are uploading your backups to S3 you will probably want to create a new @@ -373,6 +375,8 @@ For more information see similar questions on postgresql issue tracker[here](htt This documentation is for GitLab CE. We backup GitLab.com and make sure your data is secure, but you can't use these methods to export / backup your data yourself from GitLab.com. +Issues are stored in the database. They can't be stored in Git itself. + To migrate your repositories from one server to another with an up-to-date version of GitLab, you can use the [import rake task](import.md) to do a mass import of the repository. Note that if you do an import rake task, rather than a backup restore, you diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md index 96d67f7b5d6..8fbcbb983e9 100644 --- a/doc/raketasks/cleanup.md +++ b/doc/raketasks/cleanup.md @@ -12,7 +12,8 @@ sudo gitlab-rake gitlab:cleanup:dirs bundle exec rake gitlab:cleanup:dirs RAILS_ENV=production ``` -Remove repositories (global only for now) from `/home/git/repositories` if they don't exist in GitLab database. +Rename repositories from `/home/git/repositories` if they don't exist in GitLab database. +The repositories get a `+orphaned+TIMESTAMP` suffix so that they cannot block new repositories from being created. ``` # omnibus-gitlab diff --git a/doc/release/monthly.md b/doc/release/monthly.md index c1ed9e3b80e..c56e99a7005 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -195,7 +195,7 @@ This can happen before tagging because Omnibus uses tags in its own repo and SHA ## Update GitLab.com with the stable version - Deploy the package (should not need downtime because of the small difference with RC1) -- Deploy the package for ci.gitlab.com +- Deploy the package for gitlab.com/ci ## Release CE, EE and CI diff --git a/doc/reply_by_email/README.md b/doc/reply_by_email/README.md deleted file mode 100644 index 5d36f5121d1..00000000000 --- a/doc/reply_by_email/README.md +++ /dev/null @@ -1,180 +0,0 @@ -# Reply by email - -GitLab can be set up to allow users to comment on issues and merge requests by replying to notification emails. - -In order to do this, you need access to an IMAP-enabled email account, with a provider or server that supports [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing). Sub-addressing is a feature where any email to `user+some_arbitrary_tag@example.com` will end up in the mailbox for `user@example.com`, and is supported by providers such as Gmail, Yahoo! Mail, Outlook.com and iCloud, as well as the [Postfix](http://www.postfix.org/) mail server which you can run on-premises. - -## Set it up - -In this example, we'll use the Gmail address `gitlab-replies@gmail.com`. If you're actually using Gmail with Reply by email, make sure you have [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) and [allow less secure apps to access the account](https://support.google.com/accounts/answer/6010255). - -### Installations from source - -1. Go to the GitLab installation directory: - - ```sh - cd /home/git/gitlab - ``` - -1. Find the `reply_by_email` section in `config/gitlab.yml`, enable the feature and enter the email address including a placeholder for the `reply_key`: - - ```sh - sudo editor config/gitlab.yml - ``` - - ```yaml - reply_by_email: - enabled: true - address: "gitlab-replies+%{reply_key}@gmail.com" - ``` - - As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-replies@gmail.com`. - -2. Find `config/mail_room.yml.example` and copy it to `config/mail_room.yml`: - - ```sh - sudo cp config/mail_room.yml.example config/mail_room.yml - ``` - -3. Uncomment the configuration options in `config/mail_room.yml` and fill in the details for your specific IMAP server and email account: - - ```sh - sudo editor config/mail_room.yml - ``` - - ```yaml - :mailboxes: - - - # IMAP server host - :host: "imap.gmail.com" - # IMAP server port - :port: 993 - # Whether the IMAP server uses SSL - :ssl: true - # Email account username. Usually the full email address. - :email: "gitlab-replies@gmail.com" - # Email account password - :password: "[REDACTED]" - # The name of the mailbox where incoming mail will end up. Usually "inbox". - :name: "inbox" - # Always "sidekiq". - :delivery_method: sidekiq - # Always true. - :delete_after_delivery: true - :delivery_options: - # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml. - :redis_url: redis://localhost:6379 - # Always "resque:gitlab". - :namespace: resque:gitlab - # Always "incoming_email". - :queue: incoming_email - # Always "EmailReceiverWorker" - :worker: EmailReceiverWorker - ``` - - -4. Find `lib/support/init.d/gitlab.default.example` and copy it to `/etc/default/gitlab`: - - ```sh - sudo cp lib/support/init.d/gitlab.default.example /etc/default/gitlab - ``` - -5. Edit `/etc/default/gitlab` to enable `mail_room`: - - ```sh - sudo editor /etc/default/gitlab - ``` - - ```sh - mail_room_enabled=true - ``` - -6. Restart GitLab: - - ```sh - sudo service gitlab restart - ``` - -7. Check if everything is configured correctly: - - ```sh - sudo bundle exec rake gitlab:reply_by_email:check RAILS_ENV=production - ``` - -8. Reply by email should now be working. - -### Omnibus package installations - -TODO - -### Development - -1. Go to the GitLab installation directory. - -1. Find the `reply_by_email` section in `config/gitlab.yml`, enable the feature and enter the email address including a placeholder for the `reply_key`: - - ```yaml - reply_by_email: - enabled: true - address: "gitlab-replies+%{reply_key}@gmail.com" - ``` - - As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-replies@gmail.com`. - -2. Find `config/mail_room.yml.example` and copy it to `config/mail_room.yml`: - - ```sh - sudo cp config/mail_room.yml.example config/mail_room.yml - ``` - -3. Uncomment the configuration options in `config/mail_room.yml` and fill in the details for your specific IMAP server and email account: - - ```yaml - :mailboxes: - - - # IMAP server host - :host: "imap.gmail.com" - # IMAP server port - :port: 993 - # Whether the IMAP server uses SSL - :ssl: true - # Email account username. Usually the full email address. - :email: "gitlab-replies@gmail.com" - # Email account password - :password: "[REDACTED]" - # The name of the mailbox where incoming mail will end up. Usually "inbox". - :name: "inbox" - # Always "sidekiq". - :delivery_method: sidekiq - # Always true. - :delete_after_delivery: true - :delivery_options: - # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml. - :redis_url: redis://localhost:6379 - # Always "resque:gitlab". - :namespace: resque:gitlab - # Always "incoming_email". - :queue: incoming_email - # Always "EmailReceiverWorker" - :worker: EmailReceiverWorker - ``` - -4. Uncomment the `mail_room` line in your `Procfile`: - - ```yaml - mail_room: bundle exec mail_room -q -c config/mail_room.yml - ``` - -6. Restart GitLab: - - ```sh - bundle exec foreman start - ``` - -7. Check if everything is configured correctly: - - ```sh - bundle exec rake gitlab:reply_by_email:check RAILS_ENV=development - ``` - -8. Reply by email should now be working. diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 7cdcd11c04c..b6b8000af4e 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -3,27 +3,27 @@ ## SSH keys An SSH key allows you to establish a secure connection between your -computer and GitLab. - -Before generating an SSH key, check if your system already has one by -running `cat ~/.ssh/id_rsa.pub`. If you see a long string starting with -`ssh-rsa` or `ssh-dsa`, you can skip the ssh-keygen step. +computer and GitLab. Before generating an SSH key in your shell, check if your system +already has one by running the following command: +```bash +cat ~/.ssh/id_rsa.pub +``` -To generate a new SSH key, just open your terminal and use code below. The -ssh-keygen command prompts you for a location and filename to store the key -pair and for a password. When prompted for the location and filename, you -can press enter to use the default. +If you see a long string starting with `ssh-rsa` or `ssh-dsa`, you can skip the `ssh-keygen` step. -It is a best practice to use a password for an SSH key, but it is not +Note: It is a best practice to use a password for an SSH key, but it is not required and you can skip creating a password by pressing enter. Note that the password you choose here can't be altered or retrieved. +To generate a new SSH key, use the following command: ```bash ssh-keygen -t rsa -C "$your_email" ``` +This command will prompt you for a location and filename to store the key +pair and for a password. When prompted for the location and filename, you +can press enter to use the default. -Use the code below to show your public key. - +Use the command below to show your public key: ```bash cat ~/.ssh/id_rsa.pub ``` @@ -32,7 +32,7 @@ Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your user profile. Please copy the complete key starting with `ssh-` and ending with your username and host. -Use code below to copy your public key to the clipboard. Depending on your +To copy your public key to the clipboard, use code below. Depending on your OS you'll need to use a different command: **Windows:** @@ -72,6 +72,8 @@ access can happen through being a direct member of the project, or through a group. See `def accessible_deploy_keys` in `app/models/user.rb` for more information. +Deploy keys can be shared between projects, you just need to add them to each project. + ## Applications ### Eclipse diff --git a/doc/update/6.x-or-7.x-to-7.14.md b/doc/update/6.x-or-7.x-to-7.14.md index a1488474f60..b34fb12da6f 100644 --- a/doc/update/6.x-or-7.x-to-7.14.md +++ b/doc/update/6.x-or-7.x-to-7.14.md @@ -127,7 +127,7 @@ sudo apt-get install nodejs ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v2.6.4 +sudo -u git -H git checkout v2.6.5 ``` ## 7. Install libs, migrations, etc. @@ -162,12 +162,12 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab TIP: to see what changed in `gitlab.yml.example` in this release use next command: ``` -git diff 6-0-stable:config/gitlab.yml.example 7.14-stable:config/gitlab.yml.example +git diff 6-0-stable:config/gitlab.yml.example 7-14-stable:config/gitlab.yml.example ``` * Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/config/gitlab.yml.example but with your settings. * Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/config/unicorn.rb.example but with your settings. -* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.6.0/config.yml.example but with your settings. +* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.6.5/config.yml.example but with your settings. * Copy rack attack middleware config ```bash diff --git a/doc/update/7.13-to-7.14.md b/doc/update/7.13-to-7.14.md index 7c2d3f4498a..6dd9727fb49 100644 --- a/doc/update/7.13-to-7.14.md +++ b/doc/update/7.13-to-7.14.md @@ -63,7 +63,7 @@ sudo -u git -H git checkout 7-14-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v2.6.4 +sudo -u git -H git checkout v2.6.5 ``` ### 5. Install libs, migrations, etc. diff --git a/doc/update/7.14-to-8.0.md b/doc/update/7.14-to-8.0.md index 29a38d07b3d..552216be932 100644 --- a/doc/update/7.14-to-8.0.md +++ b/doc/update/7.14-to-8.0.md @@ -10,9 +10,9 @@ months after this vulnerability became known the GitLab installation guide still contained instructions that would install the outdated, 'vulnerable' Git version 2.1.2. -Run the following command to get your current Git version. +Run the following command to get your current Git version: -``` +```sh /usr/local/bin/git --version ``` @@ -63,39 +63,70 @@ sudo -u git -H git checkout 8-0-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v2.6.4 +sudo -u git -H git checkout v2.6.5 ``` ### 5. Install gitlab-git-http-server -First we download Go 1.5 and install it into /usr/local/go. +First we download Go 1.5 and install it into `/usr/local/go`: + +```bash +curl -O --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz +echo '5817fa4b2252afdb02e11e8b9dc1d9173ef3bd5a go1.5.linux-amd64.tar.gz' | shasum -c - && \ + sudo tar -C /usr/local -xzf go1.5.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.5.linux-amd64.tar.gz +``` + +Now we download `gitlab-git-http-server` and install it in `/home/git/gitlab-git-http-server`: + +```bash +cd /home/git +sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-git-http-server.git +cd gitlab-git-http-server +sudo -u git -H make +``` + +Make sure your unicorn.rb file contains a 'listen' line for +'127.0.0.1:8080' and that this line is not commented out. + +``` +cd /home/git/gitlab +grep ^listen config/unicorn.rb - curl -O --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz - echo '5817fa4b2252afdb02e11e8b9dc1d9173ef3bd5a go1.5.linux-amd64.tar.gz' | shasum -c - && \ - sudo tar -C /usr/local -xzf go1.5.linux-amd64.tar.gz - sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ - rm go1.5.linux-amd64.tar.gz +# If there is no 'listen' line for 127.0.0.1:8080, add it: +sudo -u git tee -a config/unicorn.rb <<EOF +listen "127.0.0.1:8080", :tcp_nopush => true +EOF +``` -Now we download gitlab-git-http-server and install it in /home/git/gitlab-git-http-server. +If your Git repositories are in a directory other than `/home/git/repositories`, +you need to tell `gitlab-git-http-server` about it via `/etc/gitlab/default`. +See `lib/support/init.d/gitlab.default.example` for the options. - cd /home/git - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-git-http-server.git - cd gitlab-git-http-server - sudo -u git -H make +### 6. Copy secrets -If you put your Git repositories in a directory different from /home/git/repositories, you need to tell gitlab-git-http-server about it via /etc/gitlab/default. -See lib/support/init.d/gitlab.default.example for the options. +The `secrets.yml` file is used to store keys to encrypt sessions and encrypt secure variables. +When you run migrations make sure to store it someplace safe. +Don't store it in the same place as your database backups, +otherwise your secrets are exposed if one of your backups is compromised. -### 6. Install libs, migrations, etc. +``` +cd /home/git/gitlab +sudo -u git -H cp config/secrets.yml.example config/secrets.yml +sudo -u git -H chmod 0600 config/secrets.yml +``` + +### 7. Install libs, migrations, etc. ```bash cd /home/git/gitlab -# MySQL installations (note: the line below states '--without ... postgres') -sudo -u git -H bundle install --without development test postgres --deployment +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment -# PostgreSQL installations (note: the line below states '--without ... mysql') -sudo -u git -H bundle install --without development test mysql --deployment +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment # Run database migrations sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production @@ -107,38 +138,55 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab ``` -### 7. Update config files +### 8. Update config files #### New configuration options for `gitlab.yml` -There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`. +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: -``` +```sh git diff origin/7-14-stable:config/gitlab.yml.example origin/8-0-stable:config/gitlab.yml.example -`````` +``` -#### New NGINX configuration +The new options include configuration of GitLab CI that are now being part of GitLab CE and EE. -Because of the new gitlab-git-http-server you need to update your NGINX configuration. -If you skip this step 'git clone' and 'git push' over HTTP(S) will stop working. +#### New Nginx configuration -``` -# Remove '-ssl' twice in the diff command below if you use HTTP instead of HTTPS +Because of the new `gitlab-git-http-server` you need to update your Nginx +configuration. If you skip this step 'git clone' and 'git push' over HTTP(S) +will stop working. + +View changes between the previous recommended Nginx configuration and the +current one: + +```sh +# For HTTPS configurations git diff origin/7-14-stable:lib/support/nginx/gitlab-ssl origin/8-0-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/7-14-stable:lib/support/nginx/gitlab origin/8-0-stable:lib/support/nginx/gitlab ``` -### 8. Start application +If you are using Apache instead of NGINX please see the updated [Apache templates](https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache). +Also note that because Apache does not support upstreams behind Unix sockets you will need to let gitlab-git-http-server listen on a TCP port. You can do this via [/etc/default/gitlab](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-0-stable/lib/support/init.d/gitlab.default.example#L34). + +### 9. Migrate GitLab CI to GitLab CE/EE + +Now, GitLab CE and EE has CI integrated. However, migrations don't happen automatically and you need to do it manually. +Please follow the following guide [to migrate](../migrate_ci_to_ce/README.md) your GitLab CI instance to GitLab CE/EE. + +### 10. Start application sudo service gitlab start sudo service nginx restart -### 9. Check application status +### 11. Check application status Check if GitLab and its environment are configured correctly: sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production -To make sure you didn't miss anything run a more thorough check with: +To make sure you didn't miss anything run a more thorough check: sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production @@ -147,13 +195,25 @@ If all items are green, then congratulations, the upgrade is complete! ## Things went south? Revert to previous version (7.14) ### 1. Revert the code to the previous version + Follow the [upgrade guide from 7.13 to 7.14](7.13-to-7.14.md), except for the database migration (The backup is already migrated to the previous version) -### 2. Restore from the backup: +### 2. Restore from the backup ```bash cd /home/git/gitlab sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production ``` -If you have more than one backup *.tar file(s) please add `BACKUP=timestamp_of_backup` to the command above. + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. + +## Troubleshooting + +### "You appear to have cloned an empty repository." + +If you see this message when attempting to clone a repository hosted by GitLab, +this is likely due to an outdated Nginx or Apache configuration, or a missing or +misconfigured `gitlab-git-http-server` instance. Double-check that you correctly +completed [Step 5](#5-install-gitlab-git-http-server) to install the daemon and +[Step 8](#new-nginx-configuration) to reconfigure Nginx. diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index 22b9be059d6..a66a863f6c4 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -1,7 +1,7 @@ # Universal update guide for patch versions *Make sure you view this [upgrade guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/patch_versions.md) from the `master` branch for the most up to date instructions.* -For example from 6.2.0 to 6.2.1, also see the [semantic versioning specification](http://semver.org/). +For example from 7.14.0 to 7.14.3, also see the [semantic versioning specification](http://semver.org/). ### 0. Backup @@ -23,17 +23,16 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production cd /home/git/gitlab sudo -u git -H git fetch --all sudo -u git -H git checkout -- Gemfile.lock db/schema.rb -sudo -u git -H git checkout LATEST_TAG +LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`) +sudo -u git -H git checkout $LATEST_TAG -b $LATEST_TAG ``` -Replace LATEST_TAG with the latest GitLab tag you want to upgrade to, for example `v6.6.3`. - ### 3. Update gitlab-shell to the corresponding version ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` +sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` ``` ### 4. Install libs, migrations, etc. diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md index 6854250dab7..fd0327686b1 100644 --- a/doc/update/upgrader.md +++ b/doc/update/upgrader.md @@ -1,4 +1,4 @@ -# GitLab Upgrader +# GitLab Upgrader (deprecated) *DEPRECATED* We recommend to [switch to the Omnibus package and repository server](https://about.gitlab.com/update/) instead of using this script. diff --git a/doc/web_hooks/ssl.png b/doc/web_hooks/ssl.png Binary files differnew file mode 100644 index 00000000000..698f1a0f64a --- /dev/null +++ b/doc/web_hooks/ssl.png diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index 09400d9b163..c185ccfcac3 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -6,7 +6,15 @@ You can configure web hooks to listen for specific events like pushes, issues or Web hooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server. -If you send a web hook to an SSL endpoint [the certificate will not be verified](https://gitlab.com/gitlab-org/gitlab-ce/blob/ccd617e58ea71c42b6b073e692447d0fe3c00be6/app/models/web_hook.rb#L35) since many people use self-signed certificates. +## SSL Verification + +By default, the SSL certificate of the webhook endpoint is verified based on +an internal list of Certificate Authorities, +which means the certificate cannot be self-signed. + +You can turn this off in the web hook settings in your GitLab projects. + + ## Push events @@ -34,7 +42,7 @@ X-Gitlab-Event: Push Hook "name": "Diaspora", "url": "git@example.com:mike/diasporadiaspora.git", "description": "", - "homepage": "http://example.com/mike/diaspora", + "homepage": "http://example.com/mike/diaspora", "git_http_url":"http://example.com/mike/diaspora.git", "git_ssh_url":"git@example.com:mike/diaspora.git", "visibility_level":0 @@ -121,6 +129,12 @@ X-Gitlab-Event: Issue Hook "username": "root", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" }, + "repository": { + "name": "Gitlab Test", + "url": "http://example.com/gitlabhq/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlabhq/gitlab-test" + }, "object_attributes": { "id": 301, "title": "New API: create/update/delete file", @@ -507,8 +521,8 @@ server.mount_proc '/' do |req, res| puts req.body end -trap 'INT' do - server.shutdown +trap 'INT' do + server.shutdown end server.start ``` @@ -523,4 +537,4 @@ When you press 'Test Hook' in GitLab, you should see something like this in the {"before":"077a85dd266e6f3573ef7e9ef8ce3343ad659c4e","after":"95cd4a99e93bc4bbabacfa2cd10e6725b1403c60",<SNIP>} example.com - - [14/May/2014:07:45:26 EDT] "POST / HTTP/1.1" 200 0 - -> / -``` +```
\ No newline at end of file diff --git a/doc/workflow/importing/README.md b/doc/workflow/importing/README.md index 5cde90993d2..7ccf06fbd60 100644 --- a/doc/workflow/importing/README.md +++ b/doc/workflow/importing/README.md @@ -3,6 +3,7 @@ 1. [Bitbucket](import_projects_from_bitbucket.md)
2. [GitHub](import_projects_from_github.md)
3. [GitLab.com](import_projects_from_gitlab_com.md)
+4. [FogBugz](import_projects_from_fogbugz.md)
4. [SVN](migrating_from_svn.md)
### Note
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png Binary files differnew file mode 100644 index 00000000000..205c515bd3f --- /dev/null +++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png Binary files differnew file mode 100644 index 00000000000..a1e348d46ad --- /dev/null +++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png Binary files differnew file mode 100644 index 00000000000..ed362846909 --- /dev/null +++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png Binary files differnew file mode 100644 index 00000000000..d2fbd0267bd --- /dev/null +++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png Binary files differnew file mode 100644 index 00000000000..b1cc4b58525 --- /dev/null +++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png diff --git a/doc/workflow/importing/import_projects_from_fogbugz.md b/doc/workflow/importing/import_projects_from_fogbugz.md new file mode 100644 index 00000000000..71af0f9ea44 --- /dev/null +++ b/doc/workflow/importing/import_projects_from_fogbugz.md @@ -0,0 +1,29 @@ +# Import your project from FogBugz to GitLab + +It only takes a few simple steps to import your project from FogBugz. +The importer will import all of your cases and comments with original case +numbers and timestamps. You will also have the opportunity to map FogBugz +users to GitLab users. + +* From your GitLab dashboard click 'New project' + +* Click on the 'FogBugz' button + + + +* Enter your FogBugz URL, email address, and password. + + + +* Create mapping from FogBugz users to GitLab users. + + + +* Select the projects you wish to import by clicking the Import buttons + + + +* Once the import has finished click the link to take you to the project +dashboard. Follow the directions to push your existing repository. + + diff --git a/docker/Dockerfile b/docker/Dockerfile index 05521af6963..304bb97409e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -25,6 +25,9 @@ RUN mkdir -p /opt/gitlab/sv/sshd/supervise \ && ln -s /opt/gitlab/sv/sshd /opt/gitlab/service \ && mkdir -p /var/run/sshd +# Disabling use DNS in ssh since it tends to slow connecting +RUN echo "UseDNS no" >> /etc/ssh/sshd_config + # Prepare default configuration RUN ( \ echo "" && \ diff --git a/features/admin/labels.feature b/features/admin/labels.feature new file mode 100644 index 00000000000..1af0e700bd4 --- /dev/null +++ b/features/admin/labels.feature @@ -0,0 +1,38 @@ +Feature: Admin Issues Labels + Background: + Given I sign in as an admin + And I have labels: "bug", "feature", "enhancement" + Given I visit admin labels page + + Scenario: I should see labels list + Then I should see label 'bug' + And I should see label 'feature' + + Scenario: I create new label + Given I submit new label 'support' + Then I should see label 'support' + + Scenario: I edit label + Given I visit 'bug' label edit page + When I change label 'bug' to 'fix' + Then I should not see label 'bug' + Then I should see label 'fix' + + Scenario: I remove label + When I remove label 'bug' + Then I should not see label 'bug' + + @javascript + Scenario: I delete all labels + When I delete all labels + Then I should see labels help message + + Scenario: I create a label with invalid color + Given I visit admin new label page + When I submit new label with invalid color + Then I should see label color error message + + Scenario: I create a label that already exists + Given I visit admin new label page + When I submit new label 'bug' + Then I should see label exist error message diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index 392d4235eff..b667b587c5b 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -4,12 +4,14 @@ Feature: Dashboard Given I sign in as a user And I own project "Shop" And project "Shop" has push event + And project "Shop" has CI enabled + And project "Shop" has CI build And I visit dashboard page - @javascript Scenario: I should see projects list Then I should see "New Project" link Then I should see "Shop" project link + Then I should see "Shop" project CI status @javascript Scenario: I should see activity list diff --git a/features/explore/groups.feature b/features/explore/groups.feature index c11634bd74a..a42e59c98f2 100644 --- a/features/explore/groups.feature +++ b/features/explore/groups.feature @@ -3,20 +3,6 @@ Feature: Explore Groups Background: Given group "TestGroup" has private project "Enterprise" - Scenario: I should not see group with private projects as visitor - When I visit group "TestGroup" page - Then I should be redirected to sign in page - - Scenario: I should not see group with private projects group as user - When I sign in as a user - And I visit group "TestGroup" page - Then page status code should be 404 - - Scenario: I should not see group with private and internal projects as visitor - Given group "TestGroup" has internal project "Internal" - When I visit group "TestGroup" page - Then I should be redirected to sign in page - Scenario: I should see group with private and internal projects as user Given group "TestGroup" has internal project "Internal" When I sign in as a user diff --git a/features/groups.feature b/features/groups.feature index d5272fdddcf..db37fa3b375 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -159,3 +159,14 @@ Feature: Groups When I visit group "Owned" projects page Then I should see group "Owned" projects list And I should see "archived" label + + # Public group + @javascript + Scenario: Signed out user should see group + Given "Mary Jane" is owner of group "Owned" + And I am a signed out user + And Group "Owned" has a public project "Public-project" + When I visit group "Owned" page + Then I should see group "Owned" + Then I should see project "Public-project" + diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature index 3ebc8a39aae..34161b81d44 100644 --- a/features/project/commits/commits.feature +++ b/features/project/commits/commits.feature @@ -16,6 +16,11 @@ Feature: Project Commits Then I see commit info And I see side-by-side diff button + Scenario: I browse commit with ci from list + Given commit has ci status + And I click on commit link + Then I see commit ci info + Scenario: I browse commit with side-by-side diff view Given I click on commit link And I click side-by-side diff button diff --git a/features/project/graph.feature b/features/project/graph.feature index 89064242c1c..2acd65aea5f 100644 --- a/features/project/graph.feature +++ b/features/project/graph.feature @@ -12,3 +12,9 @@ Feature: Project Graph Scenario: I should see project commits graphs When I visit project "Shop" commits graph page Then page should have commits graphs + + @javascript + Scenario: I should see project ci graphs + Given project "Shop" has CI enabled + When I visit project "Shop" CI graph page + Then page should have CI graphs diff --git a/features/project/issues/milestones.feature b/features/project/issues/milestones.feature index bfbaaec5a35..c1a20e9b488 100644 --- a/features/project/issues/milestones.feature +++ b/features/project/issues/milestones.feature @@ -12,13 +12,17 @@ Feature: Project Issues Milestones Given I click link "v2.2" Then I should see milestone "v2.2" - Scenario: I create new milestone + @javascript + Scenario: I create and delete new milestone Given I click link "New Milestone" And I submit new milestone "v2.3" Then I should see milestone "v2.3" + Given I click link to remove milestone + When I visit project "Shop" activity page + Then I should see deleted milestone activity Scenario: I delete new milestone - Given I click link to remove milestone "v2.2" + Given I click link to remove milestone And I should see no milestones @javascript diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 947f668e432..83055188bac 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -115,40 +115,40 @@ Feature: Project Merge Requests Given project "Shop" have "Bug NS-05" open merge request with diffs inside And I visit merge request page "Bug NS-05" And I click on the Changes tab - And I leave a comment like "Line is wrong" on line 39 of the second file - And I click link "Hide inline discussion" of the second file - Then I should not see a comment like "Line is wrong here" in the second file + And I leave a comment like "Line is wrong" on line 39 of the third file + And I click link "Hide inline discussion" of the third file + Then I should not see a comment like "Line is wrong here" in the third file @javascript Scenario: I show comments on a merge request diff with comments in a single file Given project "Shop" have "Bug NS-05" open merge request with diffs inside And I visit merge request page "Bug NS-05" And I click on the Changes tab - And I leave a comment like "Line is wrong" on line 39 of the second file - Then I should see a comment like "Line is wrong" in the second file + And I leave a comment like "Line is wrong" on line 39 of the third file + Then I should see a comment like "Line is wrong" in the third file @javascript Scenario: I hide comments on a merge request diff with comments in multiple files Given project "Shop" have "Bug NS-05" open merge request with diffs inside And I visit merge request page "Bug NS-05" And I click on the Changes tab - And I leave a comment like "Line is correct" on line 12 of the first file - And I leave a comment like "Line is wrong" on line 39 of the second file - And I click link "Hide inline discussion" of the second file - Then I should not see a comment like "Line is wrong here" in the second file - And I should still see a comment like "Line is correct" in the first file + And I leave a comment like "Line is correct" on line 12 of the second file + And I leave a comment like "Line is wrong" on line 39 of the third file + And I click link "Hide inline discussion" of the third file + Then I should not see a comment like "Line is wrong here" in the third file + And I should still see a comment like "Line is correct" in the second file @javascript Scenario: I show comments on a merge request diff with comments in multiple files Given project "Shop" have "Bug NS-05" open merge request with diffs inside And I visit merge request page "Bug NS-05" And I click on the Changes tab - And I leave a comment like "Line is correct" on line 12 of the first file - And I leave a comment like "Line is wrong" on line 39 of the second file - And I click link "Hide inline discussion" of the second file - And I click link "Show inline discussion" of the second file - Then I should see a comment like "Line is wrong" in the second file - And I should still see a comment like "Line is correct" in the first file + And I leave a comment like "Line is correct" on line 12 of the second file + And I leave a comment like "Line is wrong" on line 39 of the third file + And I click link "Hide inline discussion" of the third file + And I click link "Show inline discussion" of the third file + Then I should see a comment like "Line is wrong" in the third file + And I should still see a comment like "Line is correct" in the second file @javascript Scenario: I unfold diff @@ -163,8 +163,8 @@ Feature: Project Merge Requests Given project "Shop" have "Bug NS-05" open merge request with diffs inside And I visit merge request page "Bug NS-05" And I click on the Changes tab - And I leave a comment like "Line is correct" on line 12 of the first file - And I leave a comment like "Line is wrong" on line 39 of the second file + And I leave a comment like "Line is correct" on line 12 of the second file + And I leave a comment like "Line is wrong" on line 39 of the third file And I click Side-by-side Diff tab Then I should see comments on the side-by-side diff page diff --git a/features/project/project.feature b/features/project/project.feature index 089ffcba14a..b3fb0794547 100644 --- a/features/project/project.feature +++ b/features/project/project.feature @@ -74,3 +74,9 @@ Feature: Project Given I disable snippets in project When I visit project "Shop" page Then I should not see "Snippets" button + + @javascript + Scenario: I edit Project Notifications + Given I click notifications drop down button + When I choose Mention setting + Then I should see Notification saved message diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index d3a77466a35..58574166ef3 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -35,6 +35,29 @@ Feature: Project Source Browse Files And I should see its new content @javascript + Scenario: I can upload file and commit + Given I click on "new file" link in repo + Then I can see new file page + And I can see "upload an existing one" + And I click on "upload" + And I upload a new text file + And I fill the upload file commit message + And I click on "Upload file" + Then I can see the new text file + And I can see the new commit message + + @javascript + Scenario: I can replace file and commit + Given I click on ".gitignore" file in repo + And I see the ".gitignore" + And I click on "Replace" + And I replace it with a text file + And I fill the replace file commit message + And I click on "Replace file" + Then I can see the new text file + And I can see the replacement commit message + + @javascript Scenario: I can create and commit file and specify new branch Given I click on "new file" link in repo And I edit code diff --git a/features/steps/admin/labels.rb b/features/steps/admin/labels.rb new file mode 100644 index 00000000000..b45d98658bc --- /dev/null +++ b/features/steps/admin/labels.rb @@ -0,0 +1,117 @@ +class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedPaths + + step 'I visit \'bug\' label edit page' do + visit edit_admin_label_path(bug_label) + end + + step 'I visit admin new label page' do + visit new_admin_label_path + end + + step 'I visit admin labels page' do + visit admin_labels_path + end + + step 'I remove label \'bug\'' do + page.within "#label_#{bug_label.id}" do + click_link 'Remove' + end + end + + step 'I have labels: "bug", "feature", "enhancement"' do + ["bug", "feature", "enhancement"].each do |title| + Label.create(title: title, template: true) + end + end + + step 'I delete all labels' do + page.within '.labels' do + page.all('.btn-remove').each do |remove| + remove.click + sleep 0.05 + end + end + end + + step 'I should see labels help message' do + page.within '.labels' do + expect(page).to have_content 'There are no labels yet' + end + end + + step 'I submit new label \'support\'' do + visit new_admin_label_path + fill_in 'Title', with: 'support' + fill_in 'Background Color', with: '#F95610' + click_button 'Save' + end + + step 'I submit new label \'bug\'' do + visit new_admin_label_path + fill_in 'Title', with: 'bug' + fill_in 'Background Color', with: '#F95610' + click_button 'Save' + end + + step 'I submit new label with invalid color' do + visit new_admin_label_path + fill_in 'Title', with: 'support' + fill_in 'Background Color', with: '#12' + click_button 'Save' + end + + step 'I should see label exist error message' do + page.within '.label-form' do + expect(page).to have_content 'Title has already been taken' + end + end + + step 'I should see label color error message' do + page.within '.label-form' do + expect(page).to have_content 'Color is invalid' + end + end + + step 'I should see label \'feature\'' do + page.within '.manage-labels-list' do + expect(page).to have_content 'feature' + end + end + + step 'I should see label \'bug\'' do + page.within '.manage-labels-list' do + expect(page).to have_content 'bug' + end + end + + step 'I should not see label \'bug\'' do + page.within '.manage-labels-list' do + expect(page).not_to have_content 'bug' + end + end + + step 'I should see label \'support\'' do + page.within '.manage-labels-list' do + expect(page).to have_content 'support' + end + end + + step 'I change label \'bug\' to \'fix\'' do + fill_in 'Title', with: 'fix' + fill_in 'Background Color', with: '#F15610' + click_button 'Save' + end + + step 'I should see label \'fix\'' do + page.within '.manage-labels-list' do + expect(page).to have_content 'fix' + end + end + + def bug_label + Label.templates.find_or_create_by(title: 'bug') + end +end diff --git a/features/steps/admin/settings.rb b/features/steps/admin/settings.rb index 7a6aec23af8..6acbf46eb20 100644 --- a/features/steps/admin/settings.rb +++ b/features/steps/admin/settings.rb @@ -7,6 +7,7 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps step 'I modify settings and save form' do uncheck 'Gravatar enabled' fill_in 'Home page URL', with: 'https://about.gitlab.com/' + fill_in 'Help page text', with: 'Example text' click_button 'Save' end diff --git a/features/steps/admin/users.rb b/features/steps/admin/users.rb index 2e17d5c4c2e..4bc290b6bdf 100644 --- a/features/steps/admin/users.rb +++ b/features/steps/admin/users.rb @@ -4,11 +4,13 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps include SharedAdmin before do - allow(Devise).to receive(:omniauth_providers).and_return([:twitter, :twitter_updated]) + allow(Gitlab::OAuth::Provider).to receive(:providers).and_return([:twitter, :twitter_updated]) + allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_return(root_path) end after do - allow(Devise).to receive(:omniauth_providers).and_call_original + allow(Gitlab::OAuth::Provider).to receive(:providers).and_call_original + allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_call_original end step 'I should see all users' do diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index cb3a80cac29..f0fbd8a826a 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -11,6 +11,10 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps expect(page).to have_link "Shop" end + step 'I should see "Shop" project CI status' do + expect(page).to have_link "Build status: skipped" + end + step 'I should see last push widget' do expect(page).to have_content "You pushed to fix" expect(page).to have_link "Create Merge Request" diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 18a1c4d32ce..95bc9baf8d8 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -6,7 +6,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps include Select2Helper step 'I should see back to dashboard button' do - expect(page).to have_content 'Back to Dashboard' + expect(page).to have_content 'Back to dashboard' end step 'gitlab user "Mike"' do @@ -17,6 +17,26 @@ class Spinach::Features::Groups < Spinach::FeatureSteps find(:css, 'button.btn-new').click end + step 'I should see group "Owned"' do + expect(page).to have_content '@owned' + end + + step 'I am a signed out user' do + logout + end + + step 'Group "Owned" has a public project "Public-project"' do + group = Group.find_by(name: "Owned") + + @project = create :empty_project, :public, + group: group, + name: "Public-project" + end + + step 'I should see project "Public-project"' do + expect(page).to have_content 'Public-project' + end + step 'I select "Mike" as "Reporter"' do user = User.find_by(name: "Mike") diff --git a/features/steps/invites.rb b/features/steps/invites.rb index 5e8feff5095..dac972172aa 100644 --- a/features/steps/invites.rb +++ b/features/steps/invites.rb @@ -63,7 +63,7 @@ class Spinach::Features::Invites < Spinach::FeatureSteps end step 'I should be redirected to the dashboard' do - expect(current_path).to eq(dashboard_path) + expect(current_path).to eq(dashboard_projects_path) end step 'I should see a notice telling me I have declined' do diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 23e67371f96..47f58091b93 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -101,4 +101,13 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps step 'I click side-by-side diff button' do find('#parallel-diff-btn').click end + + step 'commit has ci status' do + @project.enable_ci(@user) + create :ci_commit, gl_project: @project, sha: sample_commit.id + end + + step 'I see commit ci info' do + expect(page).to have_content "build: skipped" + end end diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index 0e433781d7a..b0230add34f 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -5,8 +5,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps step 'I click link "Fork"' do expect(page).to have_content "Shop" - expect(page).to have_content "Fork" - click_link "Fork" + click_link "Fork project" end step 'I am a member of project "Shop"' do @@ -15,7 +14,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I should see the forked project page' do - expect(page).to have_content "Project was successfully forked." + expect(page).to have_content "Forked from" end step 'I already have a project named "Shop" in my namespace' do diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb index 5e7e573a6ab..9453d636445 100644 --- a/features/steps/project/graph.rb +++ b/features/steps/project/graph.rb @@ -7,12 +7,10 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps end When 'I visit project "Shop" graph page' do - project = Project.find_by(name: "Shop") visit namespace_project_graph_path(project.namespace, project, "master") end step 'I visit project "Shop" commits graph page' do - project = Project.find_by(name: "Shop") visit commits_namespace_project_graph_path(project.namespace, project, "master") end @@ -20,4 +18,20 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps expect(page).to have_content "Commit statistics for master" expect(page).to have_content "Commits per day of month" end + + step 'I visit project "Shop" CI graph page' do + visit ci_namespace_project_graph_path(project.namespace, project, 'master') + end + + step 'page should have CI graphs' do + expect(page).to have_content 'Overall' + expect(page).to have_content 'Builds chart for last week' + expect(page).to have_content 'Builds chart for last month' + expect(page).to have_content 'Builds chart for last year' + expect(page).to have_content 'Commit duration in minutes for last 30 commits' + end + + def project + project ||= Project.find_by(name: "Shop") + end end diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb index 61e62c2adbd..c8708572ec6 100644 --- a/features/steps/project/issues/milestones.rb +++ b/features/steps/project/issues/milestones.rb @@ -49,6 +49,11 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps create(:closed_issue, project: project, milestone: milestone) end + step 'I should see deleted milestone activity' do + expect(page).to have_content('opened milestone in') + expect(page).to have_content('destroyed milestone in') + end + When 'I click link "All Issues"' do click_link 'All Issues' end @@ -57,7 +62,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps expect(page).to have_selector('#tab-issues li.issue-row', count: 4) end - step 'I click link to remove milestone "v2.2"' do + step 'I click link to remove milestone' do click_link 'Remove' end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index c92998631ff..875bf6c4676 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -224,43 +224,43 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end end - step 'I click link "Hide inline discussion" of the second file' do - page.within '.files [id^=diff]:nth-child(2)' do + step 'I click link "Hide inline discussion" of the third file' do + page.within '.files [id^=diff]:nth-child(3)' do find('.js-toggle-diff-comments').trigger('click') end end - step 'I click link "Show inline discussion" of the second file' do - page.within '.files [id^=diff]:nth-child(2)' do + step 'I click link "Show inline discussion" of the third file' do + page.within '.files [id^=diff]:nth-child(3)' do find('.js-toggle-diff-comments').trigger('click') end end - step 'I should not see a comment like "Line is wrong" in the second file' do - page.within '.files [id^=diff]:nth-child(2)' do + step 'I should not see a comment like "Line is wrong" in the third file' do + page.within '.files [id^=diff]:nth-child(3)' do expect(page).not_to have_visible_content "Line is wrong" end end - step 'I should see a comment like "Line is wrong" in the second file' do - page.within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do + step 'I should see a comment like "Line is wrong" in the third file' do + page.within '.files [id^=diff]:nth-child(3) .note-body > .note-text' do expect(page).to have_visible_content "Line is wrong" end end - step 'I should not see a comment like "Line is wrong here" in the second file' do - page.within '.files [id^=diff]:nth-child(2)' do + step 'I should not see a comment like "Line is wrong here" in the third file' do + page.within '.files [id^=diff]:nth-child(3)' do expect(page).not_to have_visible_content "Line is wrong here" end end - step 'I should see a comment like "Line is wrong here" in the second file' do - page.within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do + step 'I should see a comment like "Line is wrong here" in the third file' do + page.within '.files [id^=diff]:nth-child(3) .note-body > .note-text' do expect(page).to have_visible_content "Line is wrong here" end end - step 'I leave a comment like "Line is correct" on line 12 of the first file' do + step 'I leave a comment like "Line is correct" on line 12 of the second file' do init_diff_note_first_file page.within(".js-discussion-note-form") do @@ -268,12 +268,12 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps click_button "Add Comment" end - page.within ".files [id^=diff]:nth-child(1) .note-body > .note-text" do + page.within ".files [id^=diff]:nth-child(2) .note-body > .note-text" do expect(page).to have_content "Line is correct" end end - step 'I leave a comment like "Line is wrong" on line 39 of the second file' do + step 'I leave a comment like "Line is wrong" on line 39 of the third file' do init_diff_note_second_file page.within(".js-discussion-note-form") do @@ -282,8 +282,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end end - step 'I should still see a comment like "Line is correct" in the first file' do - page.within '.files [id^=diff]:nth-child(1) .note-body > .note-text' do + step 'I should still see a comment like "Line is correct" in the second file' do + page.within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do expect(page).to have_visible_content "Line is correct" end end @@ -303,7 +303,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should see comments on the side-by-side diff page' do - page.within '.files [id^=diff]:nth-child(1) .parallel .note-body > .note-text' do + page.within '.files [id^=diff]:nth-child(2) .parallel .note-body > .note-text' do expect(page).to have_visible_content "Line is correct" end end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 0404fd5e594..15f77734cb2 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -124,10 +124,24 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see back to dashboard button' do - expect(page).to have_content 'Back to Dashboard' + expect(page).to have_content 'Back to dashboard' end step 'I should see back to group button' do - expect(page).to have_content 'Back to Group' + expect(page).to have_content 'Back to group' + end + + step 'I click notifications drop down button' do + click_link 'notifications-button' + end + + step 'I choose Mention setting' do + click_link 'On mention' + end + + step 'I should see Notification saved message' do + page.within '.flash-container' do + expect(page).to have_content 'Notification settings saved' + end end end diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index 0327fd61981..d3b462bfd31 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -26,13 +26,11 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps step 'I fill gitlab-ci settings' do check 'Active' - fill_in 'Project url', with: 'http://ci.gitlab.org/projects/3' - fill_in 'Token', with: 'verySecret' click_button 'Save' end step 'I should see service settings saved' do - expect(find_field('Project url').value).to eq 'http://ci.gitlab.org/projects/3' + expect(find_field('Active').value).to eq '1' end step 'I click hipchat service link' do diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 5cb085db207..a1a49dd58a6 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -1,3 +1,4 @@ +# coding: utf-8 class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps include SharedAuthentication include SharedProject @@ -78,7 +79,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I fill the commit message' do - fill_in :commit_message, with: 'Not yet a commit message.' + fill_in :commit_message, with: 'Not yet a commit message.', visible: true end step 'I click link "Diff"' do @@ -97,6 +98,14 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps click_button 'Remove file' end + step 'I click on "Replace"' do + click_button "Replace" + end + + step 'I click on "Replace file"' do + click_button 'Replace file' + end + step 'I see diff' do expect(page).to have_css '.line_holder.new' end @@ -106,10 +115,55 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I can see new file page' do - expect(page).to have_content "New file" + expect(page).to have_content "new file" expect(page).to have_content "Commit message" end + step 'I can see "upload an existing one"' do + expect(page).to have_content "upload an existing one" + end + + step 'I click on "upload"' do + click_link 'upload' + end + + step 'I click on "Upload file"' do + click_button 'Upload file' + end + + step 'I can see the new commit message' do + expect(page).to have_content "New upload commit message" + end + + step 'I upload a new text file' do + drop_in_dropzone test_text_file + end + + step 'I fill the upload file commit message' do + page.within('#modal-upload-blob') do + fill_in :commit_message, with: 'New upload commit message' + end + end + + step 'I replace it with a text file' do + drop_in_dropzone test_text_file + end + + step 'I fill the replace file commit message' do + page.within('#modal-upload-blob') do + fill_in :commit_message, with: 'Replacement file commit message' + end + end + + step 'I can see the replacement commit message' do + expect(page).to have_content "Replacement file commit message" + end + + step 'I can see the new text file' do + expect(page).to have_content "Lorem ipsum dolor sit amet" + expect(page).to have_content "Sed ut perspiciatis unde omnis" + end + step 'I click on files directory' do click_link 'files' end @@ -232,4 +286,29 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps def new_file_name 'not_a_file.md' end + + def drop_in_dropzone(file_path) + # Generate a fake input selector + page.execute_script <<-JS + var fakeFileInput = window.$('<input/>').attr( + {id: 'fakeFileInput', type: 'file'} + ).appendTo('body'); + JS + # Attach the file to the fake input selector with Capybara + attach_file("fakeFileInput", file_path) + # Add the file to a fileList array and trigger the fake drop event + page.execute_script <<-JS + var fileList = [$('#fakeFileInput')[0].files[0]]; + var e = jQuery.Event('drop', { dataTransfer : { files : fileList } }); + $('.dropzone')[0].dropzone.listeners[0].events.drop(e); + JS + end + + def test_text_file + File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') + end + + def test_image_file + File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') + end end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index b4deccb6520..eb978620da6 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -68,7 +68,7 @@ module SharedPaths # ---------------------------------------- step 'I visit dashboard page' do - visit dashboard_path + visit dashboard_projects_path end step 'I visit dashboard activity page' do @@ -460,7 +460,7 @@ module SharedPaths end step 'I visit snippets page' do - visit snippets_path + visit explore_snippets_path end step 'I visit new snippet page' do diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index ccbe8f96a4c..fc51cec150e 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -51,6 +51,11 @@ module SharedProject visit namespace_project_path(project.namespace, project) end + step 'I visit project "Shop" activity page' do + project = Project.find_by(name: 'Shop') + visit namespace_project_path(project.namespace, project) + end + step 'project "Shop" has push event' do @project = Project.find_by(name: "Shop") @@ -191,4 +196,14 @@ module SharedProject create(:label, project: project, title: 'feature') create(:label, project: project, title: 'enhancement') end + + step 'project "Shop" has CI enabled' do + project = Project.find_by(name: "Shop") + project.enable_ci(@user) + end + + step 'project "Shop" has CI build' do + project = Project.find_by(name: "Shop") + create :ci_commit, gl_project: project, sha: project.commit.sha + end end diff --git a/features/steps/snippets/user.rb b/features/steps/snippets/user.rb index 007fcb2893f..dea3256229f 100644 --- a/features/steps/snippets/user.rb +++ b/features/steps/snippets/user.rb @@ -4,7 +4,7 @@ class Spinach::Features::SnippetsUser < Spinach::FeatureSteps include SharedSnippet step 'I visit my snippets page' do - visit user_snippets_path(current_user) + visit dashboard_snippets_path end step 'I should see "Personal snippet one" in snippets' do diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 8dddcd7ccc3..9620d36ac41 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -8,7 +8,7 @@ module API expose :id, :state, :avatar_url expose :web_url do |user, options| - Rails.application.routes.url_helpers.user_url(user) + Gitlab::Application.routes.url_helpers.user_url(user) end end @@ -45,7 +45,7 @@ module API class ProjectHook < Hook expose :project_id, :push_events - expose :issues_events, :merge_requests_events, :tag_push_events + expose :issues_events, :merge_requests_events, :tag_push_events, :note_events, :enable_ssl_verification end class ForkedFromProject < Grape::Entity @@ -81,7 +81,7 @@ module API expose :avatar_url expose :web_url do |group, options| - Rails.application.routes.url_helpers.group_url(group) + Gitlab::Application.routes.url_helpers.group_url(group) end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 76c9cc2e3a4..7fada98fcdc 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -148,15 +148,14 @@ module API end end - def attributes_for_keys(keys) + def attributes_for_keys(keys, custom_params = nil) + params_hash = custom_params || params attrs = {} - keys.each do |key| if params[key].present? or (params.has_key?(key) and params[key] == false) attrs[key] = params[key] end end - ActionController::Parameters.new(attrs).permit! end @@ -246,6 +245,44 @@ module API error!({ 'message' => message }, status) end + # Projects helpers + + def filter_projects(projects) + # If the archived parameter is passed, limit results accordingly + if params[:archived].present? + projects = projects.where(archived: parse_boolean(params[:archived])) + end + + if params[:search].present? + projects = projects.search(params[:search]) + end + + if params[:ci_enabled_first].present? + projects.includes(:gitlab_ci_service). + reorder("services.active DESC, projects.#{project_order_by} #{project_sort}") + else + projects.reorder(project_order_by => project_sort) + end + end + + def project_order_by + order_fields = %w(id name path created_at updated_at last_activity_at) + + if order_fields.include?(params['order_by']) + params['order_by'] + else + 'created_at' + end + end + + def project_sort + if params["sort"] == 'asc' + :asc + else + :desc + end + end + private def add_pagination_headers(paginated, per_page) diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 7412274b045..63ea2f05438 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -55,7 +55,7 @@ module API else merge_requests end - merge_requests.reorder(issuable_order_by => issuable_sort) + merge_requests = merge_requests.reorder(issuable_order_by => issuable_sort) present paginate(merge_requests), with: Entities::MergeRequest end diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index ad4d2e65dfd..882d1a083ad 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -44,7 +44,8 @@ module API :issues_events, :merge_requests_events, :tag_push_events, - :note_events + :note_events, + :enable_ssl_verification ] @hook = user_project.hooks.new(attrs) @@ -75,7 +76,8 @@ module API :issues_events, :merge_requests_events, :tag_push_events, - :note_events + :note_events, + :enable_ssl_verification ] if @hook.update_attributes attrs diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 1f2251c9b9c..c2fb36b4143 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -11,42 +11,6 @@ module API attrs[:visibility_level] = Gitlab::VisibilityLevel::PUBLIC if !attrs[:visibility_level].present? && publik == true attrs end - - def filter_projects(projects) - # If the archived parameter is passed, limit results accordingly - if params[:archived].present? - projects = projects.where(archived: parse_boolean(params[:archived])) - end - - if params[:search].present? - projects = projects.search(params[:search]) - end - - if params[:ci_enabled_first].present? - projects.includes(:gitlab_ci_service). - reorder("services.active DESC, projects.#{project_order_by} #{project_sort}") - else - projects.reorder(project_order_by => project_sort) - end - end - - def project_order_by - order_fields = %w(id name path created_at updated_at last_activity_at) - - if order_fields.include?(params['order_by']) - params['order_by'] - else - 'created_at' - end - end - - def project_sort - if params["sort"] == 'asc' - :asc - else - :desc - end - end end # Get a projects list for authenticated user diff --git a/lib/api/services.rb b/lib/api/services.rb index 73645cedea4..6727e80ac1e 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -20,7 +20,7 @@ module API end required_attributes! validators.map(&:attributes).flatten.uniq - attrs = attributes_for_keys service_attributes + attrs = attributes_for_keys service_attributes if project_service.update_attributes(attrs.merge(active: true)) true @@ -41,7 +41,7 @@ module API attrs = service_attributes.inject({}) do |hash, key| hash.merge!(key => nil) end - + if project_service.update_attributes(attrs.merge(active: false)) true else @@ -49,6 +49,16 @@ module API end end end + + # Get <service_slug> service settings for project + # + # Example Request: + # + # GET /project/:id/services/gitlab-ci + # + get ':id/services/:service_slug' do + present project_service + end end end end diff --git a/lib/api/users.rb b/lib/api/users.rb index ee29f952246..a98d668e02d 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -3,7 +3,7 @@ module API class Users < Grape::API before { authenticate! } - resource :users do + resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do # Get a users list # # Example Request: @@ -121,6 +121,17 @@ module API User.where(username: attrs[:username]). where.not(id: user.id).count > 0 + identity_attrs = attributes_for_keys [:provider, :extern_uid] + if identity_attrs.any? + identity = user.identities.find_by(provider: identity_attrs[:provider]) + if identity + identity.update_attributes(identity_attrs) + else + identity = user.identities.build(identity_attrs) + identity.save + end + end + if user.update_attributes(attrs) present user, with: Entities::UserFull else diff --git a/lib/backup/builds.rb b/lib/backup/builds.rb new file mode 100644 index 00000000000..6f56f680bb9 --- /dev/null +++ b/lib/backup/builds.rb @@ -0,0 +1,34 @@ +module Backup + class Builds + attr_reader :app_builds_dir, :backup_builds_dir, :backup_dir + + def initialize + @app_builds_dir = Settings.gitlab_ci.builds_path + @backup_dir = Gitlab.config.backup.path + @backup_builds_dir = File.join(Gitlab.config.backup.path, 'builds') + end + + # Copy builds from builds directory to backup/builds + def dump + FileUtils.rm_rf(backup_builds_dir) + # Ensure the parent dir of backup_builds_dir exists + FileUtils.mkdir_p(Gitlab.config.backup.path) + # Fail if somebody raced to create backup_builds_dir before us + FileUtils.mkdir(backup_builds_dir, mode: 0700) + FileUtils.cp_r(app_builds_dir, backup_dir) + end + + def restore + backup_existing_builds_dir + + FileUtils.cp_r(backup_builds_dir, app_builds_dir) + end + + def backup_existing_builds_dir + timestamped_builds_path = File.join(app_builds_dir, '..', "builds.#{Time.now.to_i}") + if File.exists?(app_builds_dir) + FileUtils.mv(app_builds_dir, File.expand_path(timestamped_builds_path)) + end + end + end +end diff --git a/lib/backup/database.rb b/lib/backup/database.rb index ce75476a09b..959ac4b7868 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -25,8 +25,12 @@ module Backup when "postgresql" then $progress.print "Dumping PostgreSQL database #{config['database']} ... " pg_env - # Pass '--clean' to include 'DROP TABLE' statements in the DB dump. - system('pg_dump', '--clean', config['database'], out: db_file_name) + pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump. + if Gitlab.config.backup.pg_schema + pgsql_args << "-n" + pgsql_args << Gitlab.config.backup.pg_schema + end + system('pg_dump', *pgsql_args, config['database'], out: db_file_name) end report_success(success) abort 'Backup failed' unless success diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 13c68d9354f..5c42f25f4a2 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -45,7 +45,8 @@ module Backup directory = connection.directories.get(remote_directory) if directory.files.create(key: tar_file, body: File.open(tar_file), public: false, - multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size) + multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size, + encryption: Gitlab.config.backup.upload.encryption) $progress.puts "done".green else puts "uploading backup to #{remote_directory} failed".red @@ -55,7 +56,7 @@ module Backup def cleanup $progress.print "Deleting tmp directories ... " - + backup_contents.each do |dir| next unless File.exist?(File.join(Gitlab.config.backup.path, dir)) @@ -75,7 +76,7 @@ module Backup if keep_time > 0 removed = 0 - + Dir.chdir(Gitlab.config.backup.path) do file_list = Dir.glob('*_gitlab_backup.tar') file_list.map! { |f| $1.to_i if f =~ /(\d+)_gitlab_backup.tar/ } @@ -153,7 +154,7 @@ module Backup end def folders_to_backup - folders = %w{repositories db uploads} + folders = %w{repositories db uploads builds} if ENV["SKIP"] return folders.reject{ |folder| ENV["SKIP"].include?(folder) } diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb new file mode 100644 index 00000000000..ac6d667cf8d --- /dev/null +++ b/lib/ci/ansi2html.rb @@ -0,0 +1,224 @@ +# ANSI color library +# +# Implementation per http://en.wikipedia.org/wiki/ANSI_escape_code +module Ci + module Ansi2html + # keys represent the trailing digit in color changing command (30-37, 40-47, 90-97. 100-107) + COLOR = { + 0 => 'black', # not that this is gray in the intense color table + 1 => 'red', + 2 => 'green', + 3 => 'yellow', + 4 => 'blue', + 5 => 'magenta', + 6 => 'cyan', + 7 => 'white', # not that this is gray in the dark (aka default) color table + } + + STYLE_SWITCHES = { + bold: 0x01, + italic: 0x02, + underline: 0x04, + conceal: 0x08, + cross: 0x10, + } + + def self.convert(ansi) + Converter.new().convert(ansi) + end + + class Converter + def on_0(s) reset() end + def on_1(s) enable(STYLE_SWITCHES[:bold]) end + def on_3(s) enable(STYLE_SWITCHES[:italic]) end + def on_4(s) enable(STYLE_SWITCHES[:underline]) end + def on_8(s) enable(STYLE_SWITCHES[:conceal]) end + def on_9(s) enable(STYLE_SWITCHES[:cross]) end + + def on_21(s) disable(STYLE_SWITCHES[:bold]) end + def on_22(s) disable(STYLE_SWITCHES[:bold]) end + def on_23(s) disable(STYLE_SWITCHES[:italic]) end + def on_24(s) disable(STYLE_SWITCHES[:underline]) end + def on_28(s) disable(STYLE_SWITCHES[:conceal]) end + def on_29(s) disable(STYLE_SWITCHES[:cross]) end + + def on_30(s) set_fg_color(0) end + def on_31(s) set_fg_color(1) end + def on_32(s) set_fg_color(2) end + def on_33(s) set_fg_color(3) end + def on_34(s) set_fg_color(4) end + def on_35(s) set_fg_color(5) end + def on_36(s) set_fg_color(6) end + def on_37(s) set_fg_color(7) end + def on_38(s) set_fg_color_256(s) end + def on_39(s) set_fg_color(9) end + + def on_40(s) set_bg_color(0) end + def on_41(s) set_bg_color(1) end + def on_42(s) set_bg_color(2) end + def on_43(s) set_bg_color(3) end + def on_44(s) set_bg_color(4) end + def on_45(s) set_bg_color(5) end + def on_46(s) set_bg_color(6) end + def on_47(s) set_bg_color(7) end + def on_48(s) set_bg_color_256(s) end + def on_49(s) set_bg_color(9) end + + def on_90(s) set_fg_color(0, 'l') end + def on_91(s) set_fg_color(1, 'l') end + def on_92(s) set_fg_color(2, 'l') end + def on_93(s) set_fg_color(3, 'l') end + def on_94(s) set_fg_color(4, 'l') end + def on_95(s) set_fg_color(5, 'l') end + def on_96(s) set_fg_color(6, 'l') end + def on_97(s) set_fg_color(7, 'l') end + def on_99(s) set_fg_color(9, 'l') end + + def on_100(s) set_bg_color(0, 'l') end + def on_101(s) set_bg_color(1, 'l') end + def on_102(s) set_bg_color(2, 'l') end + def on_103(s) set_bg_color(3, 'l') end + def on_104(s) set_bg_color(4, 'l') end + def on_105(s) set_bg_color(5, 'l') end + def on_106(s) set_bg_color(6, 'l') end + def on_107(s) set_bg_color(7, 'l') end + def on_109(s) set_bg_color(9, 'l') end + + def convert(ansi) + @out = "" + @n_open_tags = 0 + reset() + + s = StringScanner.new(ansi.gsub("<", "<")) + while(!s.eos?) + if s.scan(/\e([@-_])(.*?)([@-~])/) + handle_sequence(s) + else + @out << s.scan(/./m) + end + end + + close_open_tags() + @out + end + + def handle_sequence(s) + indicator = s[1] + commands = s[2].split ';' + terminator = s[3] + + # We are only interested in color and text style changes - triggered by + # sequences starting with '\e[' and ending with 'm'. Any other control + # sequence gets stripped (including stuff like "delete last line") + return unless indicator == '[' and terminator == 'm' + + close_open_tags() + + if commands.empty?() + reset() + return + end + + evaluate_command_stack(commands) + + css_classes = [] + + unless @fg_color.nil? + fg_color = @fg_color + # Most terminals show bold colored text in the light color variant + # Let's mimic that here + if @style_mask & STYLE_SWITCHES[:bold] != 0 + fg_color.sub!(/fg-(\w{2,}+)/, 'fg-l-\1') + end + css_classes << fg_color + end + css_classes << @bg_color unless @bg_color.nil? + + STYLE_SWITCHES.each do |css_class, flag| + css_classes << "term-#{css_class}" if @style_mask & flag != 0 + end + + open_new_tag(css_classes) if css_classes.length > 0 + end + + def evaluate_command_stack(stack) + return unless command = stack.shift() + + if self.respond_to?("on_#{command}", true) + self.send("on_#{command}", stack) + end + + evaluate_command_stack(stack) + end + + def open_new_tag(css_classes) + @out << %{<span class="#{css_classes.join(' ')}">} + @n_open_tags += 1 + end + + def close_open_tags + while @n_open_tags > 0 + @out << %{</span>} + @n_open_tags -= 1 + end + end + + def reset + @fg_color = nil + @bg_color = nil + @style_mask = 0 + end + + def enable(flag) + @style_mask |= flag + end + + def disable(flag) + @style_mask &= ~flag + end + + def set_fg_color(color_index, prefix = nil) + @fg_color = get_term_color_class(color_index, ["fg", prefix]) + end + + def set_bg_color(color_index, prefix = nil) + @bg_color = get_term_color_class(color_index, ["bg", prefix]) + end + + def get_term_color_class(color_index, prefix) + color_name = COLOR[color_index] + return nil if color_name.nil? + + get_color_class(["term", prefix, color_name]) + end + + def set_fg_color_256(command_stack) + css_class = get_xterm_color_class(command_stack, "fg") + @fg_color = css_class unless css_class.nil? + end + + def set_bg_color_256(command_stack) + css_class = get_xterm_color_class(command_stack, "bg") + @bg_color = css_class unless css_class.nil? + end + + def get_xterm_color_class(command_stack, prefix) + # the 38 and 48 commands have to be followed by "5" and the color index + return unless command_stack.length >= 2 + return unless command_stack[0] == "5" + + command_stack.shift() # ignore the "5" command + color_index = command_stack.shift().to_i + + return unless color_index >= 0 + return unless color_index <= 255 + + get_color_class(["xterm", prefix, color_index]) + end + + def get_color_class(segments) + [segments].flatten.compact.join('-') + end + end + end +end diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb new file mode 100644 index 00000000000..5109c84e0ea --- /dev/null +++ b/lib/ci/api/api.rb @@ -0,0 +1,42 @@ +Dir["#{Rails.root}/lib/ci/api/*.rb"].each {|file| require file} + +module Ci + module API + class API < Grape::API + include APIGuard + version 'v1', using: :path + + rescue_from ActiveRecord::RecordNotFound do + rack_response({ 'message' => '404 Not found' }.to_json, 404) + end + + rescue_from :all do |exception| + # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60 + # why is this not wrapped in something reusable? + trace = exception.backtrace + + message = "\n#{exception.class} (#{exception.message}):\n" + message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) + message << " " << trace.join("\n ") + + API.logger.add Logger::FATAL, message + rack_response({ 'message' => '500 Internal Server Error' }, 500) + end + + before do + check_enable_flag! + end + + format :json + + helpers Helpers + helpers ::API::APIHelpers + + mount Builds + mount Commits + mount Runners + mount Projects + mount Triggers + end + end +end diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb new file mode 100644 index 00000000000..83ca1e6481c --- /dev/null +++ b/lib/ci/api/builds.rb @@ -0,0 +1,53 @@ +module Ci + module API + # Builds API + class Builds < Grape::API + resource :builds do + # Runs oldest pending build by runner - Runners only + # + # Parameters: + # token (required) - The uniq token of runner + # + # Example Request: + # POST /builds/register + post "register" do + authenticate_runner! + update_runner_last_contact + required_attributes! [:token] + not_found! unless current_runner.active? + + build = Ci::RegisterBuildService.new.execute(current_runner) + + if build + update_runner_info + present build, with: Entities::Build + else + not_found! + end + end + + # Update an existing build - Runners only + # + # Parameters: + # id (required) - The ID of a project + # state (optional) - The state of a build + # trace (optional) - The trace of a build + # Example Request: + # PUT /builds/:id + put ":id" do + authenticate_runner! + update_runner_last_contact + build = Ci::Build.where(runner_id: current_runner.id).running.find(params[:id]) + build.update_attributes(trace: params[:trace]) if params[:trace] + + case params[:state].to_s + when 'success' + build.success + when 'failed' + build.drop + end + end + end + end + end +end diff --git a/lib/ci/api/commits.rb b/lib/ci/api/commits.rb new file mode 100644 index 00000000000..bac463a5909 --- /dev/null +++ b/lib/ci/api/commits.rb @@ -0,0 +1,66 @@ +module Ci + module API + class Commits < Grape::API + resource :commits do + # Get list of commits per project + # + # Parameters: + # project_id (required) - The ID of a project + # project_token (requires) - Project token + # page (optional) + # per_page (optional) - items per request (default is 20) + # + get do + required_attributes! [:project_id, :project_token] + project = Ci::Project.find(params[:project_id]) + authenticate_project_token!(project) + + commits = project.commits.page(params[:page]).per(params[:per_page] || 20) + present commits, with: Entities::CommitWithBuilds + end + + # Create a commit + # + # Parameters: + # project_id (required) - The ID of a project + # project_token (requires) - Project token + # data (required) - GitLab push data + # + # Sample GitLab push data: + # { + # "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", + # } + # }, .... more commits + # ] + # } + # + # Example Request: + # POST /commits + post do + required_attributes! [:project_id, :data, :project_token] + project = Ci::Project.find(params[:project_id]) + authenticate_project_token!(project) + commit = Ci::CreateCommitService.new.execute(project, params[:data]) + + if commit.persisted? + present commit, with: Entities::CommitWithBuilds + else + errors = commit.errors.full_messages.join(", ") + render_api_error!(errors, 400) + end + end + end + end + end +end diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb new file mode 100644 index 00000000000..f47bc1236b8 --- /dev/null +++ b/lib/ci/api/entities.rb @@ -0,0 +1,56 @@ +module Ci + module API + module Entities + class Commit < Grape::Entity + expose :id, :ref, :sha, :project_id, :before_sha, :created_at + expose :status, :finished_at, :duration + expose :git_commit_message, :git_author_name, :git_author_email + end + + class CommitWithBuilds < Commit + expose :builds + end + + class Build < Grape::Entity + expose :id, :commands, :ref, :sha, :project_id, :repo_url, + :before_sha, :allow_git_fetch, :project_name + + expose :options do |model| + model.options + end + + expose :timeout do |model| + model.timeout + end + + expose :variables + end + + class Runner < Grape::Entity + expose :id, :token + end + + class Project < Grape::Entity + expose :id, :name, :token, :default_ref, :gitlab_url, :path, + :always_build, :polling_interval, :public, :ssh_url_to_repo, :gitlab_id + + expose :timeout do |model| + model.timeout + end + end + + class RunnerProject < Grape::Entity + expose :id, :project_id, :runner_id + end + + class WebHook < Grape::Entity + expose :id, :project_id, :url + end + + class TriggerRequest < Grape::Entity + expose :id, :variables + expose :commit, using: Commit + end + end + end +end diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb new file mode 100644 index 00000000000..8e893aa5cc6 --- /dev/null +++ b/lib/ci/api/helpers.rb @@ -0,0 +1,41 @@ +module Ci + module API + module Helpers + UPDATE_RUNNER_EVERY = 60 + + def check_enable_flag! + unless current_application_settings.ci_enabled + render_api_error!('400 (Bad request) CI is disabled', 400) + end + end + + def authenticate_runners! + forbidden! unless params[:token] == GitlabCi::REGISTRATION_TOKEN + end + + def authenticate_runner! + forbidden! unless current_runner + end + + def authenticate_project_token!(project) + forbidden! unless project.valid_token?(params[:project_token]) + end + + def update_runner_last_contact + if current_runner.contacted_at.nil? || Time.now - current_runner.contacted_at >= UPDATE_RUNNER_EVERY + current_runner.update_attributes(contacted_at: Time.now) + end + end + + def current_runner + @runner ||= Runner.find_by_token(params[:token].to_s) + end + + def update_runner_info + return unless params["info"].present? + info = attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"]) + current_runner.update(info) + end + end + end +end diff --git a/lib/ci/api/projects.rb b/lib/ci/api/projects.rb new file mode 100644 index 00000000000..d719ad9e8d5 --- /dev/null +++ b/lib/ci/api/projects.rb @@ -0,0 +1,195 @@ +module Ci + module API + # Projects API + class Projects < Grape::API + before { authenticate! } + + resource :projects do + # Register new webhook for project + # + # Parameters + # project_id (required) - The ID of a project + # web_hook (required) - WebHook URL + # Example Request + # POST /projects/:project_id/webhooks + post ":project_id/webhooks" do + required_attributes! [:web_hook] + + project = Ci::Project.find(params[:project_id]) + + unauthorized! unless can?(current_user, :admin_project, project.gl_project) + + web_hook = project.web_hooks.new({ url: params[:web_hook] }) + + if web_hook.save + present web_hook, with: Entities::WebHook + else + errors = web_hook.errors.full_messages.join(", ") + render_api_error!(errors, 400) + end + end + + # Retrieve all Gitlab CI projects that the user has access to + # + # Example Request: + # GET /projects + get do + gitlab_projects = current_user.authorized_projects + gitlab_projects = filter_projects(gitlab_projects) + gitlab_projects = paginate gitlab_projects + + ids = gitlab_projects.map { |project| project.id } + + projects = Ci::Project.where("gitlab_id IN (?)", ids).load + present projects, with: Entities::Project + end + + # Retrieve all Gitlab CI projects that the user owns + # + # Example Request: + # GET /projects/owned + get "owned" do + gitlab_projects = current_user.owned_projects + gitlab_projects = filter_projects(gitlab_projects) + gitlab_projects = paginate gitlab_projects + + ids = gitlab_projects.map { |project| project.id } + + projects = Ci::Project.where("gitlab_id IN (?)", ids).load + present projects, with: Entities::Project + end + + # Retrieve info for a Gitlab CI project + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # GET /projects/:id + get ":id" do + project = Ci::Project.find(params[:id]) + unauthorized! unless can?(current_user, :read_project, project.gl_project) + + present project, with: Entities::Project + end + + # Create Gitlab CI project using Gitlab project info + # + # Parameters: + # gitlab_id (required) - The gitlab id of the project + # default_ref - The branch to run against (defaults to `master`) + # Example Request: + # POST /projects + post do + required_attributes! [:gitlab_id] + + filtered_params = { + gitlab_id: params[:gitlab_id], + # we accept gitlab_url for backward compatibility for a while (added to 7.11) + default_ref: params[:default_ref] || 'master' + } + + project = Ci::Project.new(filtered_params) + project.build_missing_services + + if project.save + present project, with: Entities::Project + else + errors = project.errors.full_messages.join(", ") + render_api_error!(errors, 400) + end + end + + # Update a Gitlab CI project + # + # Parameters: + # id (required) - The ID of a project + # default_ref - The branch to run against (defaults to `master`) + # Example Request: + # PUT /projects/:id + put ":id" do + project = Ci::Project.find(params[:id]) + + unauthorized! unless can?(current_user, :admin_project, project.gl_project) + + attrs = attributes_for_keys [:default_ref] + + if project.update_attributes(attrs) + present project, with: Entities::Project + else + errors = project.errors.full_messages.join(", ") + render_api_error!(errors, 400) + end + end + + # Remove a Gitlab CI project + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # DELETE /projects/:id + delete ":id" do + project = Ci::Project.find(params[:id]) + + unauthorized! unless can?(current_user, :admin_project, project.gl_project) + + project.destroy + end + + # Link a Gitlab CI project to a runner + # + # Parameters: + # id (required) - The ID of a CI project + # runner_id (required) - The ID of a runner + # Example Request: + # POST /projects/:id/runners/:runner_id + post ":id/runners/:runner_id" do + project = Ci::Project.find(params[:id]) + runner = Ci::Runner.find(params[:runner_id]) + + unauthorized! unless can?(current_user, :admin_project, project.gl_project) + + options = { + project_id: project.id, + runner_id: runner.id + } + + runner_project = Ci::RunnerProject.new(options) + + if runner_project.save + present runner_project, with: Entities::RunnerProject + else + errors = project.errors.full_messages.join(", ") + render_api_error!(errors, 400) + end + end + + # Remove a Gitlab CI project from a runner + # + # Parameters: + # id (required) - The ID of a CI project + # runner_id (required) - The ID of a runner + # Example Request: + # DELETE /projects/:id/runners/:runner_id + delete ":id/runners/:runner_id" do + project = Ci::Project.find(params[:id]) + runner = Ci::Runner.find(params[:runner_id]) + + unauthorized! unless can?(current_user, :admin_project, project.gl_project) + + options = { + project_id: project.id, + runner_id: runner.id + } + + runner_project = Ci::RunnerProject.find_by(options) + + if runner_project.present? + runner_project.destroy + else + not_found! + end + end + end + end + end +end diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb new file mode 100644 index 00000000000..1466fe4356e --- /dev/null +++ b/lib/ci/api/runners.rb @@ -0,0 +1,69 @@ +module Ci + module API + # Runners API + class Runners < Grape::API + resource :runners do + # Get list of all available runners + # + # Example Request: + # GET /runners + get do + authenticate! + runners = Ci::Runner.all + + present runners, with: Entities::Runner + end + + # Delete runner + # Parameters: + # token (required) - The unique token of runner + # + # Example Request: + # GET /runners/delete + delete "delete" do + required_attributes! [:token] + authenticate_runner! + Ci::Runner.find_by_token(params[:token]).destroy + end + + # Register a new runner + # + # Note: This is an "internal" API called when setting up + # runners, so it is authenticated differently. + # + # Parameters: + # token (required) - The unique token of runner + # + # Example Request: + # POST /runners/register + post "register" do + required_attributes! [:token] + + runner = + if params[:token] == GitlabCi::REGISTRATION_TOKEN + # Create shared runner. Requires admin access + Ci::Runner.create( + description: params[:description], + tag_list: params[:tag_list], + is_shared: true + ) + elsif project = Ci::Project.find_by(token: params[:token]) + # Create a specific runner for project. + project.runners.create( + description: params[:description], + tag_list: params[:tag_list] + ) + end + + return forbidden! unless runner + + if runner.id + present runner, with: Entities::Runner + else + not_found! + end + end + end + end + end +end diff --git a/lib/ci/api/triggers.rb b/lib/ci/api/triggers.rb new file mode 100644 index 00000000000..40907d6db54 --- /dev/null +++ b/lib/ci/api/triggers.rb @@ -0,0 +1,49 @@ +module Ci + module API + # Build Trigger API + class Triggers < Grape::API + resource :projects do + # Trigger a GitLab CI project build + # + # Parameters: + # id (required) - The ID of a CI project + # ref (required) - The name of project's branch or tag + # token (required) - The uniq token of trigger + # Example Request: + # POST /projects/:id/ref/:ref/trigger + post ":id/refs/:ref/trigger" do + required_attributes! [:token] + + project = Ci::Project.find(params[:id]) + trigger = Ci::Trigger.find_by_token(params[:token].to_s) + not_found! unless project && trigger + unauthorized! unless trigger.project == project + + # validate variables + variables = params[:variables] + if variables + unless variables.is_a?(Hash) + render_api_error!('variables needs to be a hash', 400) + end + + unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) } + render_api_error!('variables needs to be a map of key-valued strings', 400) + end + + # convert variables from Mash to Hash + variables = variables.to_h + end + + # create request and trigger builds + trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables) + if trigger_request + present trigger_request, with: Entities::TriggerRequest + else + errors = 'No builds created' + render_api_error!(errors, 400) + end + end + end + end + end +end diff --git a/lib/ci/assets/.gitkeep b/lib/ci/assets/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/lib/ci/assets/.gitkeep diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb new file mode 100644 index 00000000000..915a4f526a6 --- /dev/null +++ b/lib/ci/charts.rb @@ -0,0 +1,71 @@ +module Ci + module Charts + class Chart + attr_reader :labels, :total, :success, :project, :build_times + + def initialize(project) + @labels = [] + @total = [] + @success = [] + @build_times = [] + @project = project + + collect + end + + + def push(from, to, format) + @labels << from.strftime(format) + @total << project.builds. + where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", to, from). + count(:all) + @success << project.builds. + where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", to, from). + success.count(:all) + end + end + + class YearChart < Chart + def collect + 13.times do |i| + start_month = (Date.today.years_ago(1) + i.month).beginning_of_month + end_month = start_month.end_of_month + + push(start_month, end_month, "%d %B %Y") + end + end + end + + class MonthChart < Chart + def collect + 30.times do |i| + start_day = Date.today - 30.days + i.days + end_day = Date.today - 30.days + i.day + 1.day + + push(start_day, end_day, "%d %B") + end + end + end + + class WeekChart < Chart + def collect + 7.times do |i| + start_day = Date.today - 7.days + i.days + end_day = Date.today - 7.days + i.day + 1.day + + push(start_day, end_day, "%d %B") + end + end + end + + class BuildTime < Chart + def collect + commits = project.commits.joins(:builds).where("#{Ci::Build.table_name}.finished_at is NOT NULL AND #{Ci::Build.table_name}.started_at is NOT NULL").last(30) + commits.each do |commit| + @labels << commit.short_sha + @build_times << (commit.duration / 60) + end + end + end + end +end diff --git a/lib/ci/current_settings.rb b/lib/ci/current_settings.rb new file mode 100644 index 00000000000..fd78b024970 --- /dev/null +++ b/lib/ci/current_settings.rb @@ -0,0 +1,22 @@ +module Ci + module CurrentSettings + def current_application_settings + key = :ci_current_application_settings + + RequestStore.store[key] ||= begin + if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?('ci_application_settings') + Ci::ApplicationSetting.current || Ci::ApplicationSetting.create_from_defaults + else + fake_application_settings + end + end + end + + def fake_application_settings + OpenStruct.new( + all_broken_builds: Ci::Settings.gitlab_ci['all_broken_builds'], + add_pusher: Ci::Settings.gitlab_ci['add_pusher'], + ) + end + end +end diff --git a/lib/ci/git.rb b/lib/ci/git.rb new file mode 100644 index 00000000000..7acc3f38edb --- /dev/null +++ b/lib/ci/git.rb @@ -0,0 +1,5 @@ +module Ci + module Git + BLANK_SHA = '0' * 40 + end +end diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb new file mode 100644 index 00000000000..e625e790df8 --- /dev/null +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -0,0 +1,198 @@ +module Ci + class GitlabCiYamlProcessor + class ValidationError < StandardError;end + + DEFAULT_STAGES = %w(build test deploy) + DEFAULT_STAGE = 'test' + ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables] + ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage] + + attr_reader :before_script, :image, :services, :variables + + def initialize(config) + @config = YAML.load(config) + + unless @config.is_a? Hash + raise ValidationError, "YAML should be a hash" + end + + @config = @config.deep_symbolize_keys + + initial_parsing + + validate! + end + + def builds_for_stage_and_ref(stage, ref, tag = false) + builds.select{|build| build[:stage] == stage && process?(build[:only], build[:except], ref, tag)} + end + + def builds + @jobs.map do |name, job| + build_job(name, job) + end + end + + def stages + @stages || DEFAULT_STAGES + end + + private + + def initial_parsing + @before_script = @config[:before_script] || [] + @image = @config[:image] + @services = @config[:services] + @stages = @config[:stages] || @config[:types] + @variables = @config[:variables] || {} + @config.except!(*ALLOWED_YAML_KEYS) + + # anything that doesn't have script is considered as unknown + @config.each do |name, param| + raise ValidationError, "Unknown parameter: #{name}" unless param.is_a?(Hash) && param.has_key?(:script) + end + + unless @config.values.any?{|job| job.is_a?(Hash)} + raise ValidationError, "Please define at least one job" + end + + @jobs = {} + @config.each do |key, job| + stage = job[:stage] || job[:type] || DEFAULT_STAGE + @jobs[key] = { stage: stage }.merge(job) + end + end + + def process?(only_params, except_params, ref, tag) + return true if only_params.nil? && except_params.nil? + + if only_params + return true if tag && only_params.include?("tags") + return true if !tag && only_params.include?("branches") + + only_params.find do |pattern| + match_ref?(pattern, ref) + end + else + return false if tag && except_params.include?("tags") + return false if !tag && except_params.include?("branches") + + except_params.each do |pattern| + return false if match_ref?(pattern, ref) + end + end + end + + def build_job(name, job) + { + stage: job[:stage], + script: "#{@before_script.join("\n")}\n#{normalize_script(job[:script])}", + tags: job[:tags] || [], + name: name, + only: job[:only], + except: job[:except], + allow_failure: job[:allow_failure] || false, + options: { + image: job[:image] || @image, + services: job[:services] || @services + }.compact + } + end + + def match_ref?(pattern, ref) + if pattern.first == "/" && pattern.last == "/" + Regexp.new(pattern[1...-1]) =~ ref + else + pattern == ref + end + end + + def normalize_script(script) + if script.is_a? Array + script.join("\n") + else + script + end + end + + def validate! + unless validate_array_of_strings(@before_script) + raise ValidationError, "before_script should be an array of strings" + end + + unless @image.nil? || @image.is_a?(String) + raise ValidationError, "image should be a string" + end + + unless @services.nil? || validate_array_of_strings(@services) + raise ValidationError, "services should be an array of strings" + end + + unless @stages.nil? || validate_array_of_strings(@stages) + raise ValidationError, "stages should be an array of strings" + end + + unless @variables.nil? || validate_variables(@variables) + raise ValidationError, "variables should be a map of key-valued strings" + end + + @jobs.each do |name, job| + validate_job!("#{name} job", job) + end + + true + end + + def validate_job!(name, job) + job.keys.each do |key| + unless ALLOWED_JOB_KEYS.include? key + raise ValidationError, "#{name}: unknown parameter #{key}" + end + end + + if !job[:script].is_a?(String) && !validate_array_of_strings(job[:script]) + raise ValidationError, "#{name}: script should be a string or an array of a strings" + end + + if job[:stage] + unless job[:stage].is_a?(String) && job[:stage].in?(stages) + raise ValidationError, "#{name}: stage parameter should be #{stages.join(", ")}" + end + end + + if job[:image] && !job[:image].is_a?(String) + raise ValidationError, "#{name}: image should be a string" + end + + if job[:services] && !validate_array_of_strings(job[:services]) + raise ValidationError, "#{name}: services should be an array of strings" + end + + if job[:tags] && !validate_array_of_strings(job[:tags]) + raise ValidationError, "#{name}: tags parameter should be an array of strings" + end + + if job[:only] && !validate_array_of_strings(job[:only]) + raise ValidationError, "#{name}: only parameter should be an array of strings" + end + + if job[:except] && !validate_array_of_strings(job[:except]) + raise ValidationError, "#{name}: except parameter should be an array of strings" + end + + if job[:allow_failure] && !job[:allow_failure].in?([true, false]) + raise ValidationError, "#{name}: allow_failure parameter should be an boolean" + end + end + + private + + def validate_array_of_strings(values) + values.is_a?(Array) && values.all? {|tag| tag.is_a?(String)} + end + + def validate_variables(variables) + variables.is_a?(Hash) && variables.all? {|key, value| key.is_a?(Symbol) && value.is_a?(String)} + end + end +end diff --git a/lib/ci/migrate/builds.rb b/lib/ci/migrate/builds.rb new file mode 100644 index 00000000000..c4f62e55295 --- /dev/null +++ b/lib/ci/migrate/builds.rb @@ -0,0 +1,29 @@ +module Ci + module Migrate + class Builds + attr_reader :app_builds_dir, :backup_builds_tarball, :backup_dir + + def initialize + @app_builds_dir = Settings.gitlab_ci.builds_path + @backup_dir = Gitlab.config.backup.path + @backup_builds_tarball = File.join(backup_dir, 'builds/builds.tar.gz') + end + + def restore + backup_existing_builds_dir + + FileUtils.mkdir_p(app_builds_dir, mode: 0700) + unless system('tar', '-C', app_builds_dir, '-zxf', backup_builds_tarball) + abort 'Restore failed'.red + end + end + + def backup_existing_builds_dir + timestamped_builds_path = File.join(app_builds_dir, '..', "builds.#{Time.now.to_i}") + if File.exists?(app_builds_dir) + FileUtils.mv(app_builds_dir, File.expand_path(timestamped_builds_path)) + end + end + end + end +end diff --git a/lib/ci/migrate/database.rb b/lib/ci/migrate/database.rb new file mode 100644 index 00000000000..bf9b80f1f62 --- /dev/null +++ b/lib/ci/migrate/database.rb @@ -0,0 +1,67 @@ +require 'yaml' + +module Ci + module Migrate + class Database + attr_reader :config + + def initialize + @config = YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))[Rails.env] + end + + def restore + decompress_rd, decompress_wr = IO.pipe + decompress_pid = spawn(*%W(gzip -cd), out: decompress_wr, in: db_file_name) + decompress_wr.close + + restore_pid = case config["adapter"] + when /^mysql/ then + $progress.print "Restoring MySQL database #{config['database']} ... " + # Workaround warnings from MySQL 5.6 about passwords on cmd line + ENV['MYSQL_PWD'] = config["password"].to_s if config["password"] + spawn('mysql', *mysql_args, config['database'], in: decompress_rd) + when "postgresql" then + $progress.print "Restoring PostgreSQL database #{config['database']} ... " + pg_env + spawn('psql', config['database'], in: decompress_rd) + end + decompress_rd.close + + success = [decompress_pid, restore_pid].all? { |pid| Process.waitpid(pid); $?.success? } + abort 'Restore failed' unless success + end + + protected + + def db_file_name + File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz') + end + + def mysql_args + args = { + 'host' => '--host', + 'port' => '--port', + 'socket' => '--socket', + 'username' => '--user', + 'encoding' => '--default-character-set' + } + args.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact + end + + def pg_env + ENV['PGUSER'] = config["username"] if config["username"] + ENV['PGHOST'] = config["host"] if config["host"] + ENV['PGPORT'] = config["port"].to_s if config["port"] + ENV['PGPASSWORD'] = config["password"].to_s if config["password"] + end + + def report_success(success) + if success + puts '[DONE]'.green + else + puts '[FAILED]'.red + end + end + end + end +end diff --git a/lib/ci/migrate/manager.rb b/lib/ci/migrate/manager.rb new file mode 100644 index 00000000000..e5e4fb784eb --- /dev/null +++ b/lib/ci/migrate/manager.rb @@ -0,0 +1,72 @@ +module Ci + module Migrate + class Manager + CI_IMPORT_PREFIX = '8.0' # Only allow imports from CI 8.0.x + + def cleanup + $progress.print "Deleting tmp directories ... " + + backup_contents.each do |dir| + next unless File.exist?(File.join(Gitlab.config.backup.path, dir)) + + if FileUtils.rm_rf(File.join(Gitlab.config.backup.path, dir)) + $progress.puts "done".green + else + puts "deleting tmp directory '#{dir}' failed".red + abort 'Backup failed' + end + end + end + + def unpack + Dir.chdir(Gitlab.config.backup.path) + + # check for existing backups in the backup dir + file_list = Dir.glob("*_gitlab_ci_backup.tar").each.map { |f| f.split(/_/).first.to_i } + puts "no backups found" if file_list.count == 0 + + if file_list.count > 1 && ENV["BACKUP"].nil? + puts "Found more than one backup, please specify which one you want to restore:" + puts "rake gitlab:backup:restore BACKUP=timestamp_of_backup" + exit 1 + end + + tar_file = ENV["BACKUP"].nil? ? File.join("#{file_list.first}_gitlab_ci_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_ci_backup.tar") + + unless File.exists?(tar_file) + puts "The specified CI backup doesn't exist!" + exit 1 + end + + $progress.print "Unpacking backup ... " + + unless Kernel.system(*%W(tar -xf #{tar_file})) + puts "unpacking backup failed".red + exit 1 + else + $progress.puts "done".green + end + + ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0 + + # restoring mismatching backups can lead to unexpected problems + if !settings[:gitlab_version].start_with?(CI_IMPORT_PREFIX) + puts "GitLab CI version mismatch:".red + puts " Your current GitLab CI version (#{GitlabCi::VERSION}) differs from the GitLab CI (#{settings[:gitlab_version]}) version in the backup!".red + exit 1 + end + end + + private + + def backup_contents + ["db", "builds", "backup_information.yml"] + end + + def settings + @settings ||= YAML.load_file("backup_information.yml") + end + end + end +end + diff --git a/lib/ci/migrate/tags.rb b/lib/ci/migrate/tags.rb new file mode 100644 index 00000000000..97e043ece27 --- /dev/null +++ b/lib/ci/migrate/tags.rb @@ -0,0 +1,42 @@ +require 'yaml' + +module Ci + module Migrate + class Tags + def restore + puts 'Inserting tags...' + connection.select_all('SELECT ci_tags.name FROM ci_tags').each do |tag| + begin + connection.execute("INSERT INTO tags (name) VALUES(#{ActiveRecord::Base::sanitize(tag['name'])})") + rescue ActiveRecord::RecordNotUnique + end + end + + ActiveRecord::Base.transaction do + puts 'Deleting old taggings...' + connection.execute "DELETE FROM taggings WHERE context = 'tags' AND taggable_type LIKE 'Ci::%'" + + puts 'Inserting taggings...' + connection.execute( + 'INSERT INTO taggings (taggable_type, taggable_id, tag_id, context) ' + + "SELECT CONCAT('Ci::', ci_taggings.taggable_type), ci_taggings.taggable_id, tags.id, 'tags' FROM ci_taggings " + + 'JOIN ci_tags ON ci_tags.id = ci_taggings.tag_id ' + + 'JOIN tags ON tags.name = ci_tags.name ' + ) + + puts 'Resetting counters... ' + connection.execute( + 'UPDATE tags SET ' + + 'taggings_count = (SELECT COUNT(*) FROM taggings WHERE tags.id = taggings.tag_id)' + ) + end + end + + protected + + def connection + ActiveRecord::Base.connection + end + end + end +end diff --git a/lib/ci/model.rb b/lib/ci/model.rb new file mode 100644 index 00000000000..c42a0ad36db --- /dev/null +++ b/lib/ci/model.rb @@ -0,0 +1,11 @@ +module Ci + module Model + def table_name_prefix + "ci_" + end + + def model_name + @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last) + end + end +end diff --git a/lib/ci/scheduler.rb b/lib/ci/scheduler.rb new file mode 100644 index 00000000000..ee0958f4be1 --- /dev/null +++ b/lib/ci/scheduler.rb @@ -0,0 +1,16 @@ +module Ci + class Scheduler + def perform + projects = Ci::Project.where(always_build: true).all + projects.each do |project| + last_commit = project.commits.last + next unless last_commit && last_commit.last_build + + interval = project.polling_interval + if (last_commit.last_build.created_at + interval.hours) < Time.now + last_commit.retry + end + end + end + end +end diff --git a/lib/ci/static_model.rb b/lib/ci/static_model.rb new file mode 100644 index 00000000000..bb2bdbed495 --- /dev/null +++ b/lib/ci/static_model.rb @@ -0,0 +1,49 @@ +# Provides an ActiveRecord-like interface to a model whose data is not persisted to a database. +module Ci + module StaticModel + extend ActiveSupport::Concern + + module ClassMethods + # Used by ActiveRecord's polymorphic association to set object_id + def primary_key + 'id' + end + + # Used by ActiveRecord's polymorphic association to set object_type + def base_class + self + end + end + + # Used by AR for fetching attributes + # + # Pass it along if we respond to it. + def [](key) + send(key) if respond_to?(key) + end + + def to_param + id + end + + def new_record? + false + end + + def persisted? + false + end + + def destroyed? + false + end + + def ==(other) + if other.is_a? ::Ci::StaticModel + id == other.id + else + super + end + end + end +end diff --git a/lib/ci/version_info.rb b/lib/ci/version_info.rb new file mode 100644 index 00000000000..2a87c91db5e --- /dev/null +++ b/lib/ci/version_info.rb @@ -0,0 +1,52 @@ +class VersionInfo + include Comparable + + attr_reader :major, :minor, :patch + + def self.parse(str) + if str && m = str.match(/(\d+)\.(\d+)\.(\d+)/) + VersionInfo.new(m[1].to_i, m[2].to_i, m[3].to_i) + else + VersionInfo.new + end + end + + def initialize(major = 0, minor = 0, patch = 0) + @major = major + @minor = minor + @patch = patch + end + + def <=>(other) + return unless other.is_a? VersionInfo + return unless valid? && other.valid? + + if other.major < @major + 1 + elsif @major < other.major + -1 + elsif other.minor < @minor + 1 + elsif @minor < other.minor + -1 + elsif other.patch < @patch + 1 + elsif @patch < other.patch + -1 + else + 0 + end + end + + def to_s + if valid? + "%d.%d.%d" % [@major, @minor, @patch] + else + "Unknown" + end + end + + def valid? + @major >= 0 && @minor >= 0 && @patch >= 0 && @major + @minor + @patch > 0 + end +end diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index 355fbd27898..2b252b32887 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -65,7 +65,7 @@ module Gitlab def reply_key reply_key = nil message.to.each do |address| - reply_key = Gitlab::ReplyByEmail.reply_key_from_address(address) + reply_key = Gitlab::IncomingEmail.key_from_address(address) break if reply_key end @@ -98,7 +98,8 @@ module Gitlab note: reply, noteable_type: sent_notification.noteable_type, noteable_id: sent_notification.noteable_id, - commit_id: sent_notification.commit_id + commit_id: sent_notification.commit_id, + line_code: sent_notification.line_code ).execute end end diff --git a/lib/gitlab/fogbugz_import/client.rb b/lib/gitlab/fogbugz_import/client.rb new file mode 100644 index 00000000000..431d50882fd --- /dev/null +++ b/lib/gitlab/fogbugz_import/client.rb @@ -0,0 +1,56 @@ +require 'fogbugz' + +module Gitlab + module FogbugzImport + class Client + attr_reader :api + + def initialize(options = {}) + if options[:uri] && options[:token] + @api = ::Fogbugz::Interface.new(options) + elsif options[:uri] && options[:email] && options[:password] + @api = ::Fogbugz::Interface.new(options) + @api.authenticate + @api + end + end + + def get_token + @api.token + end + + def valid? + !get_token.blank? + end + + def user_map + users = {} + res = @api.command(:listPeople) + res['people']['person'].each do |user| + users[user['ixPerson']] = { name: user['sFullName'], email: user['sEmail'] } + end + users + end + + def repos + res = @api.command(:listProjects) + @repos ||= res['projects']['project'].map { |proj| FogbugzImport::Repository.new(proj) } + end + + def repo(id) + repos.find { |r| r.id.to_s == id.to_s } + end + + def cases(project_id) + project_name = repo(project_id).name + res = @api.command(:search, q: "project:'#{project_name}'", cols: 'ixPersonAssignedTo,ixPersonOpenedBy,ixPersonClosedBy,sStatus,sPriority,sCategory,fOpen,sTitle,sLatestTextSummary,dtOpened,dtClosed,dtResolved,dtLastUpdated,events') + return [] unless res['cases']['count'].to_i > 0 + res['cases']['case'] + end + + def categories + @api.command(:listCategories) + end + end + end +end diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb new file mode 100644 index 00000000000..496256700b8 --- /dev/null +++ b/lib/gitlab/fogbugz_import/importer.rb @@ -0,0 +1,298 @@ +module Gitlab + module FogbugzImport + class Importer + attr_reader :project, :repo + + def initialize(project) + @project = project + + import_data = project.import_data.try(:data) + repo_data = import_data['repo'] if import_data + @repo = FogbugzImport::Repository.new(repo_data) + + @known_labels = Set.new + end + + def execute + return true unless repo.valid? + + data = project.import_data.try(:data) + + client = Gitlab::FogbugzImport::Client.new(token: data['fb_session']['token'], uri: data['fb_session']['uri']) + + @cases = client.cases(@repo.id.to_i) + @categories = client.categories + + import_cases + + true + end + + private + + def user_map + @user_map ||= begin + user_map = Hash.new + import_data = project.import_data.try(:data) + stored_user_map = import_data['user_map'] if import_data + user_map.update(stored_user_map) if stored_user_map + + user_map + end + end + + def import_labels + @categories['categories']['category'].each do |label| + create_label(label['sCategory']) + @known_labels << name + end + end + + def nice_label_color(name) + case name + when 'Blocker' + '#ff0000' + when 'Crash' + '#ffcfcf' + when 'Major' + '#deffcf' + when 'Minor' + '#cfe9ff' + when 'Bug' + '#d9534f' + when 'Feature' + '#44ad8e' + when 'Technical Task' + '#4b6dd0' + else + '#e2e2e2' + end + end + + def create_label(name) + color = nice_label_color(name) + Label.create!(project_id: project.id, title: name, color: color) + end + + def user_info(person_id) + user_hash = user_map[person_id.to_s] + + user_name = '' + gitlab_id = nil + + unless user_hash.nil? + user_name = user_hash['name'] + if user = User.find_by(id: user_hash['gitlab_user']) + user_name = "@#{user.username}" + gitlab_id = user.id + end + end + + { name: user_name, gitlab_id: gitlab_id } + end + + def import_cases + return unless @cases + + while bug = @cases.shift + author = user_info(bug['ixPersonOpenedBy'])[:name] + date = DateTime.parse(bug['dtOpened']) + + comments = bug['events']['event'] + + content = format_content(opened_content(comments)) + body = format_issue_body(author, date, content) + + labels = [] + [bug['sCategory'], bug['sPriority']].each do |label| + unless label.blank? + labels << label + unless @known_labels.include?(label) + create_label(label) + @known_labels << label + end + end + end + + assignee_id = user_info(bug['ixPersonAssignedTo'])[:gitlab_id] + author_id = user_info(bug['ixPersonOpenedBy'])[:gitlab_id] || project.creator_id + + issue = Issue.create!( + project_id: project.id, + title: bug['sTitle'], + description: body, + author_id: author_id, + assignee_id: assignee_id, + state: bug['fOpen'] == 'true' ? 'opened' : 'closed' + ) + issue.add_labels_by_names(labels) + + if issue.iid != bug['ixBug'] + issue.update_attribute(:iid, bug['ixBug']) + end + + import_issue_comments(issue, comments) + + issue.update_attribute(:created_at, date) + + last_update = DateTime.parse(bug['dtLastUpdated']) + issue.update_attribute(:updated_at, last_update) + end + end + + def opened_content(comments) + while comment = comments.shift + if comment['sVerb'] == 'Opened' + return comment['s'] + end + end + '' + end + + def import_issue_comments(issue, comments) + Note.transaction do + while comment = comments.shift + verb = comment['sVerb'] + + next if verb == 'Opened' + + content = format_content(comment['s']) + attachments = format_attachments(comment['rgAttachments']) + updates = format_updates(comment) + + next if content.blank? && attachments.empty? && updates.empty? + + author = user_info(comment['ixPerson'])[:name] + author_id = user_info(comment['ixPerson'])[:gitlab_id] || project.creator_id + date = DateTime.parse(comment['dt']) + + body = format_issue_comment_body( + comment['ixBugEvent'], + author, + date, + content, + attachments, + updates + ) + + note = Note.create!( + project_id: project.id, + noteable_type: "Issue", + noteable_id: issue.id, + author_id: author_id, + note: body + ) + + note.update_attribute(:created_at, date) + note.update_attribute(:updated_at, date) + end + end + end + + def linkify_issues(s) + s = s.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2') + s = s.gsub(/([Cc]ase) ([0-9]+)/, '\1 #\2') + s + end + + def escape_for_markdown(s) + s = s.gsub(/^#/, "\\#") + s = s.gsub(/^-/, "\\-") + s = s.gsub("`", "\\~") + s = s.gsub("\r", "") + s = s.gsub("\n", " \n") + s + end + + def format_content(raw_content) + return raw_content if raw_content.nil? + linkify_issues(escape_for_markdown(raw_content)) + end + + def format_attachments(raw_attachments) + return [] unless raw_attachments + + attachments = case raw_attachments['attachment'] + when Array + raw_attachments['attachment'] + when Hash + [raw_attachments['attachment']] + else + [] + end + + attachments.map! { |a| format_attachment(a) } + attachments.compact + end + + def format_attachment(attachment) + link = build_attachment_url(attachment['sURL']) + + res = ::Projects::DownloadService.new(project, link).execute + + return nil if res.nil? + + text = "[#{res['alt']}](#{res['url']})" + text = "!#{text}" if res['is_image'] + text + end + + def build_attachment_url(rel_url) + data = project.import_data.try(:data) + uri = data['fb_session']['uri'] + token = data['fb_session']['token'] + "#{uri}/#{rel_url}&token=#{token}" + end + + def format_updates(comment) + updates = [] + + if comment['sChanges'] + updates << "*Changes: #{linkify_issues(comment['sChanges'].chomp)}*" + end + + if comment['evtDescription'] + updates << "*#{comment['evtDescription']}*" + end + + updates + end + + def format_issue_body(author, date, content) + body = [] + body << "*By #{author} on #{date} (imported from FogBugz)*" + body << '---' + + if content.blank? + content = '*(No description has been entered for this issue)*' + end + body << content + + body.join("\n\n") + end + + def format_issue_comment_body(id, author, date, content, attachments, updates) + body = [] + body << "*By #{author} on #{date} (imported from FogBugz)*" + body << '---' + + if content.blank? + content = "*(No comment has been entered for this change)*" + end + body << content + + if updates.any? + body << '---' + body += updates + end + + if attachments.any? + body << '---' + body += attachments + end + + body.join("\n\n") + end + end + end +end diff --git a/lib/gitlab/fogbugz_import/project_creator.rb b/lib/gitlab/fogbugz_import/project_creator.rb new file mode 100644 index 00000000000..f02ea43910f --- /dev/null +++ b/lib/gitlab/fogbugz_import/project_creator.rb @@ -0,0 +1,38 @@ +module Gitlab + module FogbugzImport + class ProjectCreator + attr_reader :repo, :fb_session, :namespace, :current_user, :user_map + + def initialize(repo, fb_session, namespace, current_user, user_map = nil) + @repo = repo + @fb_session = fb_session + @namespace = namespace + @current_user = current_user + @user_map = user_map + end + + def execute + project = ::Projects::CreateService.new(current_user, + name: repo.safe_name, + path: repo.path, + namespace: namespace, + creator: current_user, + visibility_level: Gitlab::VisibilityLevel::INTERNAL, + import_type: 'fogbugz', + import_source: repo.name, + import_url: Project::UNKNOWN_IMPORT_URL + ).execute + + import_data = project.create_import_data( + data: { + 'repo' => repo.raw_data, + 'user_map' => user_map, + 'fb_session' => fb_session + } + ) + + project + end + end + end +end diff --git a/lib/gitlab/fogbugz_import/repository.rb b/lib/gitlab/fogbugz_import/repository.rb new file mode 100644 index 00000000000..d1dc63db2b2 --- /dev/null +++ b/lib/gitlab/fogbugz_import/repository.rb @@ -0,0 +1,31 @@ +module Gitlab + module FogbugzImport + class Repository + attr_accessor :raw_data + + def initialize(raw_data) + @raw_data = raw_data + end + + def valid? + raw_data.is_a?(Hash) + end + + def id + raw_data['ixProject'] + end + + def name + raw_data['sProject'] + end + + def safe_name + name.gsub(/[^\s\w.-]/, '') + end + + def path + safe_name.gsub(/[\s]/, '_') + end + end + end +end diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb index 991b70aab6a..ccfdfbe73e8 100644 --- a/lib/gitlab/import_sources.rb +++ b/lib/gitlab/import_sources.rb @@ -19,6 +19,7 @@ module Gitlab 'GitLab.com' => 'gitlab', 'Gitorious.org' => 'gitorious', 'Google Code' => 'google_code', + 'FogBugz' => 'fogbugz', 'Any repo by URL' => 'git', } end diff --git a/lib/gitlab/reply_by_email.rb b/lib/gitlab/incoming_email.rb index c3fe6778f06..856ccc71084 100644 --- a/lib/gitlab/reply_by_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -1,5 +1,5 @@ module Gitlab - module ReplyByEmail + module IncomingEmail class << self def enabled? config.enabled && address_formatted_correctly? @@ -7,20 +7,14 @@ module Gitlab def address_formatted_correctly? config.address && - config.address.include?("%{reply_key}") + config.address.include?("%{key}") end - def reply_key - return nil unless enabled? - - SecureRandom.hex(16) - end - - def reply_address(reply_key) - config.address.gsub('%{reply_key}', reply_key) + def reply_address(key) + config.address.gsub('%{key}', key) end - def reply_key_from_address(address) + def key_from_address(address) regex = address_regex return unless regex @@ -33,7 +27,7 @@ module Gitlab private def config - Gitlab.config.reply_by_email + Gitlab.config.incoming_email end def address_regex @@ -41,7 +35,7 @@ module Gitlab return nil unless wildcard_address regex = Regexp.escape(wildcard_address) - regex = regex.gsub(Regexp.escape('%{reply_key}'), "(.+)") + regex = regex.gsub(Regexp.escape('%{key}'), "(.+)") Regexp.new(regex).freeze end end diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb new file mode 100644 index 00000000000..bf4dd9542d5 --- /dev/null +++ b/lib/gitlab/ldap/auth_hash.rb @@ -0,0 +1,36 @@ +# Class to parse and transform the info provided by omniauth +# +module Gitlab + module LDAP + class AuthHash < Gitlab::OAuth::AuthHash + private + + def get_info(key) + attributes = ldap_config.attributes[key.to_s] + return super unless attributes + + attributes = Array(attributes) + + value = nil + attributes.each do |attribute| + value = get_raw(attribute) + value = value.first if value + break if value.present? + end + + return super unless value + + Gitlab::Utils.force_utf8(value) + value + end + + def get_raw(key) + auth_hash.extra[:raw_info][key] + end + + def ldap_config + @ldap_config ||= Gitlab::LDAP::Config.new(self.provider) + end + end + end +end diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index d2ffa2e1fe8..101a3285f4b 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -84,6 +84,10 @@ module Gitlab options['block_auto_created_users'] end + def attributes + options['attributes'] + end + protected def base_config Gitlab.config.ldap diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index 04a22237478..cb66fd500fe 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -71,6 +71,10 @@ module Gitlab def ldap_config Gitlab::LDAP::Config.new(auth_hash.provider) end + + def auth_hash=(auth_hash) + @auth_hash = Gitlab::LDAP::AuthHash.new(auth_hash) + end end end end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 097caf67a65..ae5f2544691 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -77,7 +77,7 @@ module Gitlab pipeline: options[:pipeline], # EmojiFilter - asset_root: Gitlab.config.gitlab.url, + asset_root: Gitlab.config.gitlab.base_url, asset_host: Gitlab::Application.config.asset_host, # TableOfContentsFilter diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb index 8613150894b..bb496135d92 100644 --- a/lib/gitlab/markdown/commit_range_reference_filter.rb +++ b/lib/gitlab/markdown/commit_range_reference_filter.rb @@ -73,7 +73,7 @@ module Gitlab end def url_for_commit_range(project, range) - h = Rails.application.routes.url_helpers + h = Gitlab::Application.routes.url_helpers h.namespace_project_compare_url(project.namespace, project, range.to_param.merge(only_path: context[:only_path])) end diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb index 5696b4fa585..fcbb2e936a5 100644 --- a/lib/gitlab/markdown/commit_reference_filter.rb +++ b/lib/gitlab/markdown/commit_reference_filter.rb @@ -69,7 +69,7 @@ module Gitlab end def url_for_commit(project, commit) - h = Rails.application.routes.url_helpers + h = Gitlab::Application.routes.url_helpers h.namespace_project_commit_url(project.namespace, project, commit, only_path: context[:only_path]) end diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb index 3d7445a27f1..1e5cb12071e 100644 --- a/lib/gitlab/markdown/label_reference_filter.rb +++ b/lib/gitlab/markdown/label_reference_filter.rb @@ -56,7 +56,7 @@ module Gitlab end def url_for_label(project, label) - h = Rails.application.routes.url_helpers + h = Gitlab::Application.routes.url_helpers h.namespace_project_issues_path(project.namespace, project, label_name: label.name, only_path: context[:only_path]) diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb index 48248f5219d..ecbd263d0e0 100644 --- a/lib/gitlab/markdown/merge_request_reference_filter.rb +++ b/lib/gitlab/markdown/merge_request_reference_filter.rb @@ -63,7 +63,7 @@ module Gitlab end def url_for_merge_request(mr, project) - h = Rails.application.routes.url_helpers + h = Gitlab::Application.routes.url_helpers h.namespace_project_merge_request_url(project.namespace, project, mr, only_path: context[:only_path]) end diff --git a/lib/gitlab/markdown/relative_link_filter.rb b/lib/gitlab/markdown/relative_link_filter.rb index 8c5cf51bfe1..6ee3d1ce039 100644 --- a/lib/gitlab/markdown/relative_link_filter.rb +++ b/lib/gitlab/markdown/relative_link_filter.rb @@ -59,25 +59,43 @@ module Gitlab end def relative_file_path(path) - nested_path = build_nested_path(path, context[:requested_path]) + nested_path = build_relative_path(path, context[:requested_path]) file_exists?(nested_path) ? nested_path : path end - # Covering a special case, when the link is referencing file in the same - # directory. - # If we are at doc/api/README.md and the README.md contains relative - # links like [Users](users.md), this takes the request - # path(doc/api/README.md) and replaces the README.md with users.md so the - # path looks like doc/api/users.md. - # If we are at doc/api and the README.md shown in below the tree view - # this takes the request path(doc/api) and adds users.md so the path - # looks like doc/api/users.md - def build_nested_path(path, request_path) + # Convert a relative path into its correct location based on the currently + # requested path + # + # path - Relative path String + # request_path - Currently-requested path String + # + # Examples: + # + # # File in the same directory as the current path + # build_relative_path("users.md", "doc/api/README.md") + # # => "doc/api/users.md" + # + # # File in the same directory, which is also the current path + # build_relative_path("users.md", "doc/api") + # # => "doc/api/users.md" + # + # # Going up one level to a different directory + # build_relative_path("../update/7.14-to-8.0.md", "doc/api/README.md") + # # => "doc/update/7.14-to-8.0.md" + # + # Returns a String + def build_relative_path(path, request_path) return request_path if path.empty? return path unless request_path parts = request_path.split('/') parts.pop if path_type(request_path) != 'tree' + + while parts.length > 1 && path.start_with?('../') + parts.pop + path.sub!('../', '') + end + parts.push(path).join('/') end diff --git a/lib/gitlab/markdown/sanitization_filter.rb b/lib/gitlab/markdown/sanitization_filter.rb index 68ed57f6257..e368de7d848 100644 --- a/lib/gitlab/markdown/sanitization_filter.rb +++ b/lib/gitlab/markdown/sanitization_filter.rb @@ -67,12 +67,16 @@ module Gitlab def clean_spans lambda do |env| - return unless env[:node_name] == 'span' - return unless env[:node].has_attribute?('class') + node = env[:node] - unless has_ancestor?(env[:node], 'pre') - env[:node].remove_attribute('class') + return unless node.name == 'span' + return unless node.has_attribute?('class') + + unless has_ancestor?(node, 'pre') + node.remove_attribute('class') end + + { node_whitelist: [node] } end end end diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb index 9e1aab936cb..e2cf89cb1d8 100644 --- a/lib/gitlab/markdown/snippet_reference_filter.rb +++ b/lib/gitlab/markdown/snippet_reference_filter.rb @@ -63,7 +63,7 @@ module Gitlab end def url_for_snippet(snippet, project) - h = Rails.application.routes.url_helpers + h = Gitlab::Application.routes.url_helpers h.namespace_project_snippet_url(project.namespace, project, snippet, only_path: context[:only_path]) end diff --git a/lib/gitlab/markdown/syntax_highlight_filter.rb b/lib/gitlab/markdown/syntax_highlight_filter.rb index 86f4385753a..8597e02f0de 100644 --- a/lib/gitlab/markdown/syntax_highlight_filter.rb +++ b/lib/gitlab/markdown/syntax_highlight_filter.rb @@ -21,7 +21,13 @@ module Gitlab language = node.attr('class') code = node.text - highlighted = block_code(code, language) + begin + highlighted = block_code(code, language) + rescue + # Gracefully handle syntax highlighter bugs/errors to ensure + # users can still access an issue/comment/etc. + highlighted = "<pre>#{code}</pre>" + 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 1871e52df0e..6f436ea7167 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 779819bc2bf..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) 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 --- /dev/null +++ b/lib/tasks/ci/.gitkeep 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 b8eb13a4fea..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} @@ -634,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 @@ -665,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 @@ -679,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") @@ -689,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 @@ -708,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 @@ -753,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 @@ -789,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 index 3f276a5e12e..39541c0b9c6 100644 --- a/lib/tasks/services.rake +++ b/lib/tasks/services.rake @@ -40,6 +40,15 @@ 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 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 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="97" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="97" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#9f9f9f" d="M37 0h60v20H37z"/><path fill="url(#b)" d="M0 0h97v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="66" y="15" fill="#010101" fill-opacity=".3">canceled</text><text x="66" y="14">canceled</text></g></svg>
\ 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 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="78" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="78" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#e05d44" d="M37 0h41v20H37z"/><path fill="url(#b)" d="M0 0h78v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="56.5" y="15" fill="#010101" fill-opacity=".3">failed</text><text x="56.5" y="14">failed</text></g></svg>
\ 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 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="92" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="92" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#dfb317" d="M37 0h55v20H37z"/><path fill="url(#b)" d="M0 0h92v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="63.5" y="15" fill="#010101" fill-opacity=".3">pending</text><text x="63.5" y="14">pending</text></g></svg>
\ 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 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="90" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="90" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#dfb317" d="M37 0h53v20H37z"/><path fill="url(#b)" d="M0 0h90v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="62.5" y="15" fill="#010101" fill-opacity=".3">running</text><text x="62.5" y="14">running</text></g></svg>
\ 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 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="97" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="97" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#9f9f9f" d="M37 0h60v20H37z"/><path fill="url(#b)" d="M0 0h97v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="66" y="15" fill="#010101" fill-opacity=".3">skipped</text><text x="66" y="14">skipped</text></g></svg>
\ 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 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="91" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="91" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#4c1" d="M37 0h54v20H37z"/><path fill="url(#b)" d="M0 0h91v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="63" y="15" fill="#010101" fill-opacity=".3">success</text><text x="63" y="14">success</text></g></svg>
\ 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 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="98" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="98" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#9f9f9f" d="M37 0h61v20H37z"/><path fill="url(#b)" d="M0 0h98v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="66.5" y="15" fill="#010101" fill-opacity=".3">unknown</text><text x="66.5" y="14">unknown</text></g></svg>
\ No newline at end of file diff --git a/public/ci/favicon.ico b/public/ci/favicon.ico Binary files differnew file mode 100644 index 00000000000..9663d4d00b9 --- /dev/null +++ b/public/ci/favicon.ico 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/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/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/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/ci/builds.rb b/spec/factories/ci/builds.rb new file mode 100644 index 00000000000..99da5a18776 --- /dev/null +++ b/spec/factories/ci/builds.rb @@ -0,0 +1,47 @@ +# == 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 + 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 + end +end diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb new file mode 100644 index 00000000000..9c7a0e9cbe0 --- /dev/null +++ b/spec/factories/ci/commits.rb @@ -0,0 +1,77 @@ +# == 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_commit, class: Ci::Commit do + ref 'master' + before_sha '76de212e80737a608d939f648d959671fb0a0142' + sha '97de212e80737a608d939f648d959671fb0a0142' + push_data do + { + ref: 'refs/heads/master', + before: '76de212e80737a608d939f648d959671fb0a0142', + after: '97de212e80737a608d939f648d959671fb0a0142', + user_name: 'Git User', + user_email: 'git@example.com', + repository: { + name: 'test-data', + url: 'ssh://git@gitlab.com/test/test-data.git', + description: '', + homepage: 'http://gitlab.com/test/test-data' + }, + commits: [ + { + id: '97de212e80737a608d939f648d959671fb0a0142', + message: 'Test commit message', + timestamp: '2014-09-23T13:12:25+02:00', + url: 'https://gitlab.com/test/test-data/commit/97de212e80737a608d939f648d959671fb0a0142', + author: { + name: 'Git User', + email: 'git@user.com' + } + } + ], + total_commits_count: 1, + ci_yaml_file: File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + } + end + + gl_project factory: :empty_project + + factory :ci_commit_without_jobs do + after(:create) do |commit, evaluator| + commit.push_data[:ci_yaml_file] = YAML.dump({}) + commit.save + end + end + + factory :ci_commit_with_one_job do + after(:create) do |commit, evaluator| + commit.push_data[:ci_yaml_file] = YAML.dump({ rspec: { script: "ls" } }) + commit.save + end + end + + factory :ci_commit_with_two_jobs do + after(:create) do |commit, evaluator| + commit.push_data[:ci_yaml_file] = YAML.dump({ rspec: { script: "ls" }, spinach: { script: "ls" } }) + commit.save + 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/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/ci/admin/builds_spec.rb b/spec/features/ci/admin/builds_spec.rb new file mode 100644 index 00000000000..ee757206a03 --- /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 + 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 + + 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..d65699dbefa --- /dev/null +++ b/spec/features/ci/builds_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe "Builds" do + context :private_project do + before do + @commit = FactoryGirl.create :ci_commit + @build = FactoryGirl.create :ci_build, commit: @commit + login_as :user + @commit.project.gl_project.team << [@user, :master] + end + + describe "GET /:project/builds/:id" do + before do + visit ci_project_build_path(@commit.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 + + 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 + @build.cancel! + visit 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 + + context :public_project do + describe "Show page public accessible" do + before do + @commit = FactoryGirl.create :ci_commit + @commit.project.public = true + @commit.project.save + + @runner = FactoryGirl.create :ci_specific_runner + @build = FactoryGirl.create :ci_build, commit: @commit, runner: @runner + + stub_gitlab_calls + visit ci_project_build_path(@commit.project, @build) + end + + it { expect(page).to have_content @commit.sha[0..7] } + end + end +end diff --git a/spec/features/ci/commits_spec.rb b/spec/features/ci/commits_spec.rb new file mode 100644 index 00000000000..657a9dabe9e --- /dev/null +++ b/spec/features/ci/commits_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe "Commits" do + include Ci::CommitsHelper + + context "Authenticated user" do + before do + @commit = FactoryGirl.create :ci_commit + @build = FactoryGirl.create :ci_build, commit: @commit + login_as :user + @commit.project.gl_project.team << [@user, :master] + end + + describe "GET /:project/commits/:sha" do + before do + visit ci_commit_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_commit_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_commit_path(@commit) + + expect(page).not_to have_content ".gitlab-ci.yml not found in this commit" + end + + it "shows warning" do + @commit.push_data[:ci_yaml_file] = nil + @commit.save + + visit ci_commit_path(@commit) + + expect(page).to have_content ".gitlab-ci.yml not found in this commit" + end + end + end + + context "Public pages" do + before do + @commit = FactoryGirl.create :ci_commit + @commit.project.public = true + @commit.project.save + + @build = FactoryGirl.create :ci_build, commit: @commit + end + + describe "GET /:project/commits/:sha" do + before do + visit ci_commit_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 + 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/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/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 5639b3db913..be0e0c747b7 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -38,6 +38,17 @@ describe GitlabMarkdownHelper do expect(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 @@ -135,4 +146,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/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/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('<div class="js-syntax-highlight"></div>') + + it 'applies syntax highlighting', -> + stubUserColorScheme('monokai') + + $('.js-syntax-highlight').syntaxHighlight() + + expect($('.js-syntax-highlight')).toHaveClass('monokai') + + describe 'on a parent element', -> + beforeEach -> + fixture.set """ + <div class="parent"> + <div class="js-syntax-highlight"></div> + <div class="foo"></div> + <div class="js-syntax-highlight"></div> + </div> + """ + + it 'applies highlighting to all applicable children', -> + stubUserColorScheme('monokai') + + $('.parent').syntaxHighlight() + + expect($('.parent, .foo')).not.toHaveClass('monokai') + expect($('.monokai').length).toBe(2) + + it 'prevents an infinite loop when no matches exist', -> + fixture.set('<div></div>') + + highlight = -> $('div').syntaxHighlight() + + expect(highlight).not.toThrow() diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb new file mode 100644 index 00000000000..75c023bbc43 --- /dev/null +++ b/spec/lib/ci/ansi2html_spec.rb @@ -0,0 +1,134 @@ +require 'spec_helper' + +describe Ci::Ansi2html do + subject { Ci::Ansi2html } + + it "prints non-ansi as-is" do + expect(subject.convert("Hello")).to eq('Hello') + end + + it "strips non-color-changing controll sequences" do + expect(subject.convert("Hello \e[2Kworld")).to eq('Hello world') + end + + it "prints simply red" do + expect(subject.convert("\e[31mHello\e[0m")).to eq('<span class="term-fg-red">Hello</span>') + end + + it "prints simply red without trailing reset" do + expect(subject.convert("\e[31mHello")).to eq('<span class="term-fg-red">Hello</span>') + end + + it "prints simply yellow" do + expect(subject.convert("\e[33mHello\e[0m")).to eq('<span class="term-fg-yellow">Hello</span>') + end + + it "prints default on blue" do + expect(subject.convert("\e[39;44mHello")).to eq('<span class="term-bg-blue">Hello</span>') + end + + it "prints red on blue" do + expect(subject.convert("\e[31;44mHello")).to eq('<span class="term-fg-red term-bg-blue">Hello</span>') + end + + it "resets colors after red on blue" do + expect(subject.convert("\e[31;44mHello\e[0m world")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> world') + end + + it "performs color change from red/blue to yellow/blue" do + expect(subject.convert("\e[31;44mHello \e[33mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-blue">world</span>') + end + + it "performs color change from red/blue to yellow/green" do + expect(subject.convert("\e[31;44mHello \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-green">world</span>') + end + + it "performs color change from red/blue to reset to yellow/green" do + expect(subject.convert("\e[31;44mHello\e[0m \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> <span class="term-fg-yellow term-bg-green">world</span>') + end + + it "ignores unsupported codes" do + expect(subject.convert("\e[51mHello\e[0m")).to eq('Hello') + end + + it "prints light red" do + expect(subject.convert("\e[91mHello\e[0m")).to eq('<span class="term-fg-l-red">Hello</span>') + end + + it "prints default on light red" do + expect(subject.convert("\e[101mHello\e[0m")).to eq('<span class="term-bg-l-red">Hello</span>') + end + + it "performs color change from red/blue to default/blue" do + expect(subject.convert("\e[31;44mHello \e[39mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>') + end + + it "performs color change from light red/blue to default/blue" do + expect(subject.convert("\e[91;44mHello \e[39mworld")).to eq('<span class="term-fg-l-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>') + end + + it "prints bold text" do + expect(subject.convert("\e[1mHello")).to eq('<span class="term-bold">Hello</span>') + end + + it "resets bold text" do + expect(subject.convert("\e[1mHello\e[21m world")).to eq('<span class="term-bold">Hello</span> world') + expect(subject.convert("\e[1mHello\e[22m world")).to eq('<span class="term-bold">Hello</span> world') + end + + it "prints italic text" do + expect(subject.convert("\e[3mHello")).to eq('<span class="term-italic">Hello</span>') + end + + it "resets italic text" do + expect(subject.convert("\e[3mHello\e[23m world")).to eq('<span class="term-italic">Hello</span> world') + end + + it "prints underlined text" do + expect(subject.convert("\e[4mHello")).to eq('<span class="term-underline">Hello</span>') + end + + it "resets underlined text" do + expect(subject.convert("\e[4mHello\e[24m world")).to eq('<span class="term-underline">Hello</span> world') + end + + it "prints concealed text" do + expect(subject.convert("\e[8mHello")).to eq('<span class="term-conceal">Hello</span>') + end + + it "resets concealed text" do + expect(subject.convert("\e[8mHello\e[28m world")).to eq('<span class="term-conceal">Hello</span> world') + end + + it "prints crossed-out text" do + expect(subject.convert("\e[9mHello")).to eq('<span class="term-cross">Hello</span>') + end + + it "resets crossed-out text" do + expect(subject.convert("\e[9mHello\e[29m world")).to eq('<span class="term-cross">Hello</span> world') + end + + it "can print 256 xterm fg colors" do + expect(subject.convert("\e[38;5;16mHello")).to eq('<span class="xterm-fg-16">Hello</span>') + end + + it "can print 256 xterm fg colors on normal magenta background" do + expect(subject.convert("\e[38;5;16;45mHello")).to eq('<span class="xterm-fg-16 term-bg-magenta">Hello</span>') + end + + it "can print 256 xterm bg colors" do + expect(subject.convert("\e[48;5;240mHello")).to eq('<span class="xterm-bg-240">Hello</span>') + end + + it "can print 256 xterm bg colors on normal magenta foreground" do + expect(subject.convert("\e[48;5;16;35mHello")).to eq('<span class="term-fg-magenta xterm-bg-16">Hello</span>') + end + + it "prints bold colored text vividly" do + expect(subject.convert("\e[1;31mHello\e[0m")).to eq('<span class="term-fg-l-red term-bold">Hello</span>') + end + + it "prints bold light colored text correctly" do + expect(subject.convert("\e[1;91mHello\e[0m")).to eq('<span class="term-fg-l-red term-bold">Hello</span>') + end +end diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb new file mode 100644 index 00000000000..83e2ad220b8 --- /dev/null +++ b/spec/lib/ci/charts_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe "Charts" do + + context "build_times" do + before do + @commit = FactoryGirl.create(:ci_commit) + FactoryGirl.create(:ci_build, commit: @commit) + end + + it 'should return build times in minutes' do + chart = Ci::Charts::BuildTime.new(@commit.project) + expect(chart.build_times).to eq([2]) + end + end +end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb new file mode 100644 index 00000000000..49482ac2b12 --- /dev/null +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -0,0 +1,313 @@ +require 'spec_helper' + +module Ci + describe GitlabCiYamlProcessor do + + describe "#builds_for_ref" do + let(:type) { 'test' } + + it "returns builds if no branch specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec" } + }) + + config_processor = GitlabCiYamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({ + stage: "test", + except: nil, + name: :rspec, + only: nil, + script: "pwd\nrspec", + tags: [], + options: {}, + allow_failure: false + }) + end + + it "does not return builds if only has another branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", only: ["deploy"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) + end + + it "does not return builds if only has regexp with another branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", only: ["/^deploy$/"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) + end + + it "returns builds if only has specified this branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", only: ["master"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) + end + + it "does not build tags" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", except: ["tags"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref(type, "0-1", true).size).to eq(0) + end + + it "returns builds if only has a list of branches including specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: ["master", "deploy"] } + }) + + config_processor = GitlabCiYamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + end + + it "returns build only for specified type" do + + config = YAML.dump({ + before_script: ["pwd"], + build: { script: "build", type: "build", only: ["master", "deploy"] }, + rspec: { script: "rspec", type: type, only: ["master", "deploy"] }, + staging: { script: "deploy", type: "deploy", only: ["master", "deploy"] }, + production: { script: "deploy", type: "deploy", only: ["master", "deploy"] }, + }) + + config_processor = GitlabCiYamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref("production", "deploy").size).to eq(0) + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) + end + end + + describe "Image and service handling" do + it "returns image and service when defined" do + config = YAML.dump({ + image: "ruby:2.1", + services: ["mysql"], + before_script: ["pwd"], + rspec: { script: "rspec" } + }) + + config_processor = GitlabCiYamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + except: nil, + stage: "test", + name: :rspec, + only: nil, + script: "pwd\nrspec", + tags: [], + options: { + image: "ruby:2.1", + services: ["mysql"] + }, + allow_failure: false + }) + end + + it "returns image and service when overridden for job" do + config = YAML.dump({ + image: "ruby:2.1", + services: ["mysql"], + before_script: ["pwd"], + rspec: { image: "ruby:2.5", services: ["postgresql"], script: "rspec" } + }) + + config_processor = GitlabCiYamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + except: nil, + stage: "test", + name: :rspec, + only: nil, + script: "pwd\nrspec", + tags: [], + options: { + image: "ruby:2.5", + services: ["postgresql"] + }, + allow_failure: false + }) + end + end + + describe "Variables" do + it "returns variables when defined" do + variables = { + var1: "value1", + var2: "value2", + } + config = YAML.dump({ + variables: variables, + before_script: ["pwd"], + rspec: { script: "rspec" } + }) + + config_processor = GitlabCiYamlProcessor.new(config) + expect(config_processor.variables).to eq(variables) + end + end + + describe "Error handling" do + it "indicates that object is invalid" do + expect{GitlabCiYamlProcessor.new("invalid_yaml\n!ccdvlf%612334@@@@")}.to raise_error(GitlabCiYamlProcessor::ValidationError) + end + + it "returns errors if tags parameter is invalid" do + config = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: tags parameter should be an array of strings") + end + + it "returns errors if before_script parameter is invalid" do + config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script should be an array of strings") + end + + it "returns errors if image parameter is invalid" do + config = YAML.dump({ image: ["test"], rspec: { script: "test" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image should be a string") + end + + it "returns errors if job image parameter is invalid" do + config = YAML.dump({ rspec: { script: "test", image: ["test"] } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: image should be a string") + end + + it "returns errors if services parameter is not an array" do + config = YAML.dump({ services: "test", rspec: { script: "test" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services should be an array of strings") + end + + it "returns errors if services parameter is not an array of strings" do + config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services should be an array of strings") + end + + it "returns errors if job services parameter is not an array" do + config = YAML.dump({ rspec: { script: "test", services: "test" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings") + end + + it "returns errors if job services parameter is not an array of strings" do + config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings") + end + + it "returns errors if there are unknown parameters" do + config = YAML.dump({ extra: "bundle update" }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra") + end + + it "returns errors if there are unknown parameters that are hashes, but doesn't have a script" do + config = YAML.dump({ extra: { services: "test" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra") + end + + it "returns errors if there is no any jobs defined" do + config = YAML.dump({ before_script: ["bundle update"] }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Please define at least one job") + end + + it "returns errors if job allow_failure parameter is not an boolean" do + config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: allow_failure parameter should be an boolean") + end + + it "returns errors if job stage is not a string" do + config = YAML.dump({ rspec: { script: "test", type: 1, allow_failure: "string" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") + end + + it "returns errors if job stage is not a pre-defined stage" do + config = YAML.dump({ rspec: { script: "test", type: "acceptance", allow_failure: "string" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") + end + + it "returns errors if job stage is not a defined stage" do + config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", type: "acceptance", allow_failure: "string" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test") + end + + it "returns errors if stages is not an array" do + config = YAML.dump({ types: "test", rspec: { script: "test" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages should be an array of strings") + end + + it "returns errors if stages is not an array of strings" do + config = YAML.dump({ types: [true, "test"], rspec: { script: "test" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages should be an array of strings") + end + + it "returns errors if variables is not a map" do + config = YAML.dump({ variables: "test", rspec: { script: "test" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings") + end + + it "returns errors if variables is not a map of key-valued strings" do + config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings") + end + end + end +end diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index 9c115bbfc6a..48bc60eed16 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe ExtractsPath do include ExtractsPath include RepoHelpers - include Rails.application.routes.url_helpers + include Gitlab::Application.routes.url_helpers let(:project) { double('project') } diff --git a/spec/lib/gitlab/backend/grack_auth_spec.rb b/spec/lib/gitlab/backend/grack_auth_spec.rb index d9676445908..829a9c197ef 100644 --- a/spec/lib/gitlab/backend/grack_auth_spec.rb +++ b/spec/lib/gitlab/backend/grack_auth_spec.rb @@ -175,12 +175,14 @@ describe Grack::Auth do context "when a gitlab ci token is provided" do let(:token) { "123" } + let(:gitlab_ci_project) { FactoryGirl.create :ci_project, token: token } before do + project.gitlab_ci_project = gitlab_ci_project + project.save + gitlab_ci_service = project.build_gitlab_ci_service gitlab_ci_service.active = true - gitlab_ci_service.token = token - gitlab_ci_service.project_url = "http://google.com" gitlab_ci_service.save env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials("gitlab-ci-token", token) diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb index 1cc80f35f98..e470b7cd5f5 100644 --- a/spec/lib/gitlab/email/receiver_spec.rb +++ b/spec/lib/gitlab/email/receiver_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" describe Gitlab::Email::Receiver do before do - stub_reply_by_email_setting(enabled: true, address: "reply+%{reply_key}@appmail.adventuretime.ooo") + stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo") end let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" } diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb new file mode 100644 index 00000000000..5fdb9c723b1 --- /dev/null +++ b/spec/lib/gitlab/incoming_email_spec.rb @@ -0,0 +1,61 @@ +require "spec_helper" + +describe Gitlab::IncomingEmail do + describe "self.enabled?" do + context "when reply by email is enabled" do + before do + stub_incoming_email_setting(enabled: true) + end + + context "when the address is valid" do + before do + stub_incoming_email_setting(address: "replies+%{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_incoming_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_incoming_email_setting(enabled: false) + end + + it "returns false" do + expect(described_class.enabled?).to be_falsey + end + end + end + + context "self.reply_address" do + before do + stub_incoming_email_setting(address: "replies+%{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.key_from_address" do + before do + stub_incoming_email_setting(address: "replies+%{key}@example.com") + end + + it "returns reply key" do + expect(described_class.key_from_address("replies+key@example.com")).to eq("key") + end + end +end diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb new file mode 100644 index 00000000000..7d8268536a4 --- /dev/null +++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Gitlab::LDAP::AuthHash do + let(:auth_hash) do + Gitlab::LDAP::AuthHash.new( + OmniAuth::AuthHash.new( + uid: '123456', + provider: 'ldapmain', + info: info, + extra: { + raw_info: raw_info + } + ) + ) + end + + let(:info) do + { + name: 'Smith, J.', + email: 'johnsmith@example.com', + nickname: '123456' + } + end + + let(:raw_info) do + { + uid: ['123456'], + email: ['johnsmith@example.com'], + cn: ['Smith, J.'], + fullName: ['John Smith'] + } + end + + context "without overridden attributes" do + + it "has the correct username" do + expect(auth_hash.username).to eq("123456") + end + + it "has the correct name" do + expect(auth_hash.name).to eq("Smith, J.") + end + end + + context "with overridden attributes" do + let(:attributes) do + { + 'username' => ['mail', 'email'], + 'name' => 'fullName' + } + end + + before do + allow_any_instance_of(Gitlab::LDAP::Config).to receive(:attributes).and_return(attributes) + end + + it "has the correct username" do + expect(auth_hash.username).to eq("johnsmith@example.com") + end + + it "has the correct name" do + expect(auth_hash.name).to eq("John Smith") + end + end +end diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index 84d9fb54b61..fd2e5f6d0e1 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -11,7 +11,7 @@ describe Gitlab::LDAP::User do } end let(:auth_hash) do - double(uid: 'my-uid', provider: 'ldapmain', info: double(info)) + OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info) end describe :changed? do diff --git a/spec/lib/gitlab/markdown/relative_link_filter_spec.rb b/spec/lib/gitlab/markdown/relative_link_filter_spec.rb index 7f4d67e403f..027336ceb73 100644 --- a/spec/lib/gitlab/markdown/relative_link_filter_spec.rb +++ b/spec/lib/gitlab/markdown/relative_link_filter_spec.rb @@ -4,14 +4,16 @@ require 'spec_helper' module Gitlab::Markdown describe RelativeLinkFilter do - def filter(doc) - described_class.call(doc, { + def filter(doc, contexts = {}) + contexts.reverse_merge!({ commit: project.commit, project: project, project_wiki: project_wiki, ref: ref, requested_path: requested_path }) + + described_class.call(doc, contexts) end def image(path) @@ -75,6 +77,22 @@ module Gitlab::Markdown to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" end + it 'rebuilds relative URL for a file in the repo up one directory' do + relative_link = link('../api/README.md') + doc = filter(relative_link, requested_path: 'doc/update/7.14-to-8.0.md') + + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + end + + it 'rebuilds relative URL for a file in the repo up multiple directories' do + relative_link = link('../../../api/README.md') + doc = filter(relative_link, requested_path: 'doc/foo/bar/baz/README.md') + + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + end + it 'rebuilds relative URL for a file in the repo with an anchor' do doc = filter(link('README.md#section')) expect(doc.at_css('a')['href']). @@ -108,8 +126,8 @@ module Gitlab::Markdown escaped = Addressable::URI.escape(path) # Stub these methods so the file doesn't actually need to be in the repo - allow_any_instance_of(described_class).to receive(:file_exists?). - and_return(true) + allow_any_instance_of(described_class). + to receive(:file_exists?).and_return(true) allow_any_instance_of(described_class). to receive(:image?).with(path).and_return(true) diff --git a/spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb b/spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb new file mode 100644 index 00000000000..6a490673728 --- /dev/null +++ b/spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe SyntaxHighlightFilter do + include FilterSpecHelper + + it 'highlights valid code blocks' do + result = filter('<pre><code>def fun end</code>') + expect(result.to_html).to eq("<pre class=\"code highlight js-syntax-highlight plaintext\"><code>def fun end</code></pre>\n") + end + + it 'passes through invalid code blocks' do + allow_any_instance_of(SyntaxHighlightFilter).to receive(:block_code).and_raise(StandardError) + + result = filter('<pre><code>This is a test</code></pre>') + expect(result.to_html).to eq('<pre>This is a test</pre>') + 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..c0083fc85be 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', 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..2c97a521d96 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -712,7 +712,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 +730,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 +748,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/ci/build_spec.rb b/spec/models/ci/build_spec.rb new file mode 100644 index 00000000000..82623bd8190 --- /dev/null +++ b/spec/models/ci/build_spec.rb @@ -0,0 +1,351 @@ +# == 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 } + + it { is_expected.to belong_to(:commit) } + it { is_expected.to validate_presence_of :status } + + 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 + 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 :ref do + subject { build.ref } + + it { is_expected.to eq(commit.ref) } + 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 :before_sha do + subject { build.before_sha } + + it { is_expected.to eq(commit.before_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 +end diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb new file mode 100644 index 00000000000..5429151c8d9 --- /dev/null +++ b/spec/models/ci/commit_spec.rb @@ -0,0 +1,273 @@ +# == 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 } + let(:commit_with_project) { FactoryGirl.create :ci_commit, gl_project: gl_project } + let(:config_processor) { Ci::GitlabCiYamlProcessor.new(gitlab_ci_yaml) } + + it { is_expected.to belong_to(:gl_project) } + it { is_expected.to have_many(:builds) } + it { is_expected.to validate_presence_of :before_sha } + it { is_expected.to validate_presence_of :sha } + it { is_expected.to validate_presence_of :ref } + it { is_expected.to validate_presence_of :push_data } + + 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 :project_recipients do + + context 'always sending notification' do + it 'should return commit_pusher_email as only recipient when no additional recipients are given' do + project = FactoryGirl.create :ci_project, + email_add_pusher: true, + email_recipients: '' + gl_project = FactoryGirl.create :empty_project, gitlab_ci_project: project + commit = FactoryGirl.create :ci_commit, gl_project: gl_project + expected = 'commit_pusher_email' + allow(commit).to receive(:push_data) { { user_email: expected } } + expect(commit.project_recipients).to eq([expected]) + end + + it 'should return commit_pusher_email and additional recipients' do + project = FactoryGirl.create :ci_project, + email_add_pusher: true, + email_recipients: 'rec1 rec2' + gl_project = FactoryGirl.create :empty_project, gitlab_ci_project: project + commit = FactoryGirl.create :ci_commit, gl_project: gl_project + expected = 'commit_pusher_email' + allow(commit).to receive(:push_data) { { user_email: expected } } + expect(commit.project_recipients).to eq(['rec1', 'rec2', expected]) + end + + it 'should return recipients' do + project = FactoryGirl.create :ci_project, + email_add_pusher: false, + email_recipients: 'rec1 rec2' + gl_project = FactoryGirl.create :empty_project, gitlab_ci_project: project + commit = FactoryGirl.create :ci_commit, gl_project: gl_project + expect(commit.project_recipients).to eq(['rec1', 'rec2']) + end + + it 'should return unique recipients only' do + project = FactoryGirl.create :ci_project, + email_add_pusher: true, + email_recipients: 'rec1 rec1 rec2' + gl_project = FactoryGirl.create :empty_project, gitlab_ci_project: project + commit = FactoryGirl.create :ci_commit, gl_project: gl_project + expected = 'rec2' + allow(commit).to receive(:push_data) { { user_email: expected } } + expect(commit.project_recipients).to eq(['rec1', 'rec2']) + end + 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 :compare? do + subject { commit_with_project.compare? } + + context 'if commit.before_sha are not nil' do + it { is_expected.to be_truthy } + end + end + + describe :short_sha do + subject { commit.short_before_sha } + + it 'has 8 items' do + expect(subject.size).to eq(8) + end + it { expect(commit.before_sha).to start_with(subject) } + 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 :create_next_builds do + before do + allow(commit).to receive(:config_processor).and_return(config_processor) + end + + it "creates builds for next type" do + expect(commit.create_builds).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(2) + + expect(commit.create_next_builds(nil)).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(4) + + expect(commit.create_next_builds(nil)).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(5) + + expect(commit.create_next_builds(nil)).to be_falsey + end + end + + describe :create_builds do + before do + allow(commit).to receive(:config_processor).and_return(config_processor) + end + + it 'creates builds' do + expect(commit.create_builds).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(2) + 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(commit.create_builds(trigger_request)).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(2) + end + + it 'rebuilds commit' do + expect(commit.create_builds).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(2) + + expect(commit.create_builds(trigger_request)).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(4) + end + + it 'creates next builds' do + expect(commit.create_builds(trigger_request)).to be_truthy + commit.builds.reload + expect(commit.builds.size).to eq(2) + + expect(commit.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 + commit.push_data[:commits][0][:message] = 'skip this commit [ci skip]' + commit.save + end + + it 'rebuilds commit' do + expect(commit.status).to eq('skipped') + expect(commit.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 + build1 = 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 + build = 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 +end diff --git a/spec/models/ci/mail_service_spec.rb b/spec/models/ci/mail_service_spec.rb new file mode 100644 index 00000000000..0d9f85959ba --- /dev/null +++ b/spec/models/ci/mail_service_spec.rb @@ -0,0 +1,190 @@ +# == 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 } + + 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) } + + 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) } + + 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) } + + 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) } + + 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) } + + 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) } + + 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/hip_chat_message_spec.rb b/spec/models/ci/project_services/hip_chat_message_spec.rb new file mode 100644 index 00000000000..1903c036924 --- /dev/null +++ b/spec/models/ci/project_services/hip_chat_message_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +describe Ci::HipChatMessage do + subject { Ci::HipChatMessage.new(build) } + + context "One build" do + let(:commit) { FactoryGirl.create(:ci_commit_with_one_job) } + + let(:build) do + commit.create_builds + commit.builds.first + end + + context 'when build succeeds' do + it 'returns a successful message' do + build.update(status: "success") + + expect( subject.status_color ).to eq 'green' + expect( subject.notify? ).to be_falsey + expect( subject.to_s ).to match(/Build '[^']+' #\d+/) + expect( subject.to_s ).to match(/Successful in \d+ second\(s\)\./) + end + end + + context 'when build fails' do + it 'returns a failure message' do + build.update(status: "failed") + + expect( subject.status_color ).to eq 'red' + expect( subject.notify? ).to be_truthy + expect( subject.to_s ).to match(/Build '[^']+' #\d+/) + expect( subject.to_s ).to match(/Failed in \d+ second\(s\)\./) + end + end + end + + context "Several builds" do + 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 + 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 + 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 +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/slack_message_spec.rb b/spec/models/ci/project_services/slack_message_spec.rb new file mode 100644 index 00000000000..7b541802d7d --- /dev/null +++ b/spec/models/ci/project_services/slack_message_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe Ci::SlackMessage do + subject { Ci::SlackMessage.new(commit) } + + context "One build" do + let(:commit) { FactoryGirl.create(:ci_commit_with_one_job) } + + let(:build) do + commit.create_builds + commit.builds.first + end + + context 'when build succeeded' do + let(:color) { 'good' } + + it 'returns a message with succeeded build' do + build.update(status: "success") + + expect(subject.color).to eq(color) + expect(subject.fallback).to include('Build') + expect(subject.fallback).to include("\##{build.id}") + expect(subject.fallback).to include('succeeded') + expect(subject.attachments.first[:fields]).to be_empty + end + end + + context 'when build failed' do + let(:color) { 'danger' } + + it 'returns a message with failed build' do + build.update(status: "failed") + + expect(subject.color).to eq(color) + expect(subject.fallback).to include('Build') + expect(subject.fallback).to include("\##{build.id}") + expect(subject.fallback).to include('failed') + expect(subject.attachments.first[:fields]).to be_empty + end + end + end + + context "Several builds" do + 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 + 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 + 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 +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/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 index bad9a9e6e1a..e9967f5fe0b 100644 --- a/spec/models/project_services/drone_ci_service_spec.rb +++ b/spec/models/project_services/drone_ci_service_spec.rb @@ -34,8 +34,6 @@ describe DroneCiService do 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('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(: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) } @@ -48,7 +46,6 @@ describe DroneCiService do 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('token with spaces').for(:token) } it { is_expected.to allow_value('ftp://drone.example.com').for(:drone_url) } 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..8cdd551a0ca 100644 --- a/spec/models/project_services/gitlab_ci_service_spec.rb +++ b/spec/models/project_services/gitlab_ci_service_spec.rb @@ -26,51 +26,21 @@ 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/projects/#{@ci_project.id}/refs/master/commits/2ab7834c")} + it { expect(@service.build_page("issue#2", 'master')).to eq("http://localhost/ci/projects/#{@ci_project.id}/refs/master/commits/issue%232")} end describe "execute" do @@ -80,8 +50,6 @@ describe GitlabCiService do 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]) @service.execute(push_sample_data) @@ -91,7 +59,7 @@ describe GitlabCiService do describe "Fork registration" do before do - @old_project = create(:empty_project) + @old_project = create(:ci_project).gl_project @project = create(:empty_project) @user = create(:user) @@ -104,9 +72,9 @@ describe GitlabCiService do ) 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) + it "creates fork on CI" do + expect_any_instance_of(Ci::CreateProjectService).to receive(:execute) + @service.fork_registration(@project, @user) end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2fcbd5ae108..ba8897b95d9 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -401,4 +401,26 @@ 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 } + let(:user) { create :user } + + before { project.enable_ci(user) } + + 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/user_spec.rb b/spec/models/user_spec.rb index eeb9069aa17..480950859a2 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -188,7 +188,7 @@ describe User do end it 'confirms a user' do - user.confirm! + user.confirm expect(user.confirmed?).to be_truthy 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 c297904614a..9aa60826f21 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -17,9 +17,9 @@ describe API::API, api: true do 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| + service_klass.validators_on(attr).any? do |v| v.class == ActiveRecord::Validations::PresenceValidator end end @@ -47,5 +47,15 @@ describe API::API, api: true do expect(project.send(service_method).activated?).to be_falsey end end + + describe "GET /projects/:id/services/#{service.dasherize}" do + include_context service + + it "should get #{service} settings" do + get api("/projects/#{project.id}/services/#{dashed_service}", user) + + 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..bad250fbf48 --- /dev/null +++ b/spec/requests/ci/api/builds_spec.rb @@ -0,0 +1,117 @@ +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) } + + 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 + 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 + + 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 + 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(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..a41c321a300 --- /dev/null +++ b/spec/requests/ci/api/commits_spec.rb @@ -0,0 +1,66 @@ +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", + } + } + ], + ci_yaml_file: gitlab_ci_yaml + } + 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..bbe98e7dacd --- /dev/null +++ b/spec/requests/ci/api/triggers_spec.rb @@ -0,0 +1,79 @@ +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!(:project) { FactoryGirl.create(:ci_project) } + let!(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: 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 + + 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 + before do + @commit = FactoryGirl.create(:ci_commit, gl_project: gl_project) + end + + 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..3ab8c915dfd --- /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_ref_commits_path(@commit.project, @commit.ref, @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..84ab0a615dd --- /dev/null +++ b/spec/services/ci/create_commit_service_spec.rb @@ -0,0 +1,145 @@ +require 'spec_helper' + +module Ci + describe CreateCommitService do + let(:service) { CreateCommitService.new } + let(:project) { FactoryGirl.create(:ci_project) } + + describe :execute do + context 'valid params' do + let(:commit) do + service.execute(project, + ref: 'refs/heads/master', + before: '00000000', + after: '31das312', + ci_yaml_file: gitlab_ci_yaml, + 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, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + ci_yaml_file: gitlab_ci_yaml, + 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"] } }) + + result = service.execute(project, + ref: 'refs/heads/0_1', + before: '00000000', + after: '31das312', + ci_yaml_file: config, + commits: [ { message: "Message" } ] + ) + expect(result).to be_persisted + end + end + + it 'fails commits without .gitlab-ci.yml' do + result = service.execute(project, + ref: 'refs/heads/0_1', + before: '00000000', + after: '31das312', + ci_yaml_file: config, + 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 + it "skips builds creation if there is [ci skip] tag in commit message" do + commits = [{ message: "some message[ci skip]" }] + commit = service.execute(project, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits, + ci_yaml_file: gitlab_ci_yaml + ) + 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 + commits = [{ message: "some message" }] + + commit = service.execute(project, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits, + ci_yaml_file: gitlab_ci_yaml + ) + + 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 + commits = [{ message: "some message[ci skip]" }] + commit = service.execute(project, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits, + ci_yaml_file: "invalid: file" + ) + 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 + commits = [{ message: "message" }] + commit = service.execute(project, + ref: 'refs/heads/master', + before: '00000000', + after: '31das312', + commits: commits, + ci_yaml_file: gitlab_ci_yaml + ) + expect(commit.builds.count(:all)).to eq(2) + + commit = service.execute(project, + ref: 'refs/heads/master', + before: '00000000', + after: '31das312', + commits: commits, + ci_yaml_file: gitlab_ci_yaml + ) + expect(commit.builds.count(:all)).to eq(2) + end + + it "creates commit with failed status if yaml is invalid" do + commits = [{ message: "some message" }] + + commit = service.execute(project, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits, + ci_yaml_file: "invalid: file" + ) + + expect(commit.status).to eq("failed") + expect(commit.builds.any?).to be false + end + end + end +end diff --git a/spec/services/ci/create_project_service_spec.rb b/spec/services/ci/create_project_service_spec.rb new file mode 100644 index 00000000000..2de7b0deca7 --- /dev/null +++ b/spec/services/ci/create_project_service_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Ci::CreateProjectService do + let(:service) { Ci::CreateProjectService.new } + let(:current_user) { double.as_null_object } + let(:project) { FactoryGirl.create :project } + + describe :execute do + context 'valid params' do + subject { service.execute(current_user, project) } + + it { is_expected.to be_kind_of(Ci::Project) } + it { is_expected.to be_persisted } + end + + context 'without project dump' do + it 'should raise exception' do + expect { service.execute(current_user, '', '') }. + to raise_error(NoMethodError) + end + end + + context "forking" do + let(:ci_origin_project) do + FactoryGirl.create(:ci_project, shared_runners_enabled: true, public: true, allow_git_fetch: true) + end + + subject { service.execute(current_user, project, ci_origin_project) } + + it "uses project as a template for settings and jobs" do + expect(subject.shared_runners_enabled).to be_truthy + expect(subject.public).to be_truthy + expect(subject.allow_git_fetch).to be_truthy + 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..525a24cc200 --- /dev/null +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe Ci::CreateTriggerRequestService do + let(:service) { Ci::CreateTriggerRequestService.new } + let(:project) { FactoryGirl.create :ci_project } + let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } + let(:trigger) { FactoryGirl.create :ci_trigger, project: project } + + describe :execute do + context 'valid params' do + subject { service.execute(project, trigger, 'master') } + + before do + @commit = FactoryGirl.create :ci_commit, gl_project: gl_project + end + + it { expect(subject).to be_kind_of(Ci::TriggerRequest) } + it { expect(subject.commit).to eq(@commit) } + 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 + FactoryGirl.create :ci_commit_without_jobs, gl_project: gl_project + end + + it { expect(subject).to be_nil } + end + + context 'for multiple commits' do + subject { service.execute(project, trigger, 'master') } + + before do + @commit1 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: gl_project + @commit2 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: gl_project + @commit3 = FactoryGirl.create :ci_commit, committed_at: 3.hour.ago, gl_project: gl_project + end + + context 'retries latest one' do + it { expect(subject).to be_kind_of(Ci::TriggerRequest) } + it { expect(subject).to be_persisted } + it { expect(subject.commit).to eq(@commit2) } + end + 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/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..18ab333c1d1 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 @@ -45,10 +44,11 @@ describe Projects::ForkService do context 'GitLab CI is enabled' do it "calls fork registrator for CI" do + create(:ci_project, gl_project: @from_project) @from_project.build_missing_services @from_project.gitlab_ci_service.update_attributes(active: true) - expect(ForkRegistrationWorker).to receive(:perform_async) + expect_any_instance_of(Ci::CreateProjectService).to receive(:execute) fork_project(@from_project, @to_user) end @@ -100,7 +100,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..dfe855926c6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -28,7 +28,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.infer_spec_type_from_file_location! config.raise_errors_for_deprecations! 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 755964e9a3d..203117aee70 100644 --- a/spec/support/filter_spec_helper.rb +++ b/spec/support/filter_spec_helper.rb @@ -72,6 +72,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 <canvas> tag ","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:sandbox/charts-js.git","http_url_to_repo":"http://demo.gitlab.com/sandbox/charts-js.git","web_url":"http://demo.gitlab.com/sandbox/charts-js","owner":{"id":8,"name":"Sandbox","created_at":"2013-08-01T16:44:17Z"},"name":"Charts.js","name_with_namespace":"Sandbox / Charts.js","path":"charts-js","path_with_namespace":"sandbox/charts-js","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-08-01T16:47:29Z","last_activity_at":"2013-12-02T15:18:11Z","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":13,"description":"","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:sandbox/afro.git","http_url_to_repo":"http://demo.gitlab.com/sandbox/afro.git","web_url":"http://demo.gitlab.com/sandbox/afro","owner":{"id":8,"name":"Sandbox","created_at":"2013-08-01T16:44:17Z"},"name":"Afro","name_with_namespace":"Sandbox / Afro","path":"afro","path_with_namespace":"sandbox/afro","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-11-14T17:45:19Z","last_activity_at":"2013-12-02T17:41:45Z","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"}}]
\ No newline at end of file diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json new file mode 100644 index 00000000000..ce8dfe5ae75 --- /dev/null +++ b/spec/support/gitlab_stubs/session.json @@ -0,0 +1,20 @@ +{ + "id":2, + "username":"jsmith", + "email":"test@test.com", + "name":"John Smith", + "bio":"", + "skype":"aertert", + "linkedin":"", + "twitter":"", + "theme_id":2,"color_scheme_id":2, + "state":"active", + "created_at":"2012-12-21T13:02:20Z", + "extern_uid":null, + "provider":null, + "is_admin":false, + "can_create_group":false, + "can_create_project":false, + "private_token":"Wvjy2Krpb7y8xi93owUz", + "access_token":"Wvjy2Krpb7y8xi93owUz" +}
\ No newline at end of file diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json new file mode 100644 index 00000000000..ce8dfe5ae75 --- /dev/null +++ b/spec/support/gitlab_stubs/user.json @@ -0,0 +1,20 @@ +{ + "id":2, + "username":"jsmith", + "email":"test@test.com", + "name":"John Smith", + "bio":"", + "skype":"aertert", + "linkedin":"", + "twitter":"", + "theme_id":2,"color_scheme_id":2, + "state":"active", + "created_at":"2012-12-21T13:02:20Z", + "extern_uid":null, + "provider":null, + "is_admin":false, + "can_create_group":false, + "can_create_project":false, + "private_token":"Wvjy2Krpb7y8xi93owUz", + "access_token":"Wvjy2Krpb7y8xi93owUz" +}
\ No newline at end of file diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index ffe30a4246c..cd9fdc6f18e 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -44,4 +44,8 @@ module LoginHelpers def logout_direct page.driver.submit :delete, '/users/sign_out', {} end + + def skip_ci_admin_auth + allow_any_instance_of(Ci::Admin::ApplicationController).to receive_messages(authenticate_admin!: true) + end end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index 9df226c3af8..7500d0fdf80 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -27,6 +27,9 @@ module MarkdownMatchers match do |actual| expect(actual).to have_selector('img.emoji', count: 10) + + image = actual.at_css('img.emoji') + expect(image['src'].to_s).to start_with(Gitlab.config.gitlab.url + '/assets') end end diff --git a/spec/support/relative_url.rb b/spec/support/relative_url.rb new file mode 100644 index 00000000000..72e3ccce75b --- /dev/null +++ b/spec/support/relative_url.rb @@ -0,0 +1,8 @@ +# Fix route helpers in tests (e.g. root_path, ...) +module RelativeUrl + extend ActiveSupport::Concern + + included do + default_url_options[:script_name] = Rails.application.config.relative_url_root + end +end diff --git a/spec/support/setup_builds_storage.rb b/spec/support/setup_builds_storage.rb new file mode 100644 index 00000000000..a3e59646187 --- /dev/null +++ b/spec/support/setup_builds_storage.rb @@ -0,0 +1,17 @@ +RSpec.configure do |config| + def builds_path + Rails.root.join('tmp/builds') + end + + config.before(:each) do + FileUtils.mkdir_p(builds_path) + FileUtils.touch(File.join(builds_path, ".gitkeep")) + Settings.gitlab_ci['builds_path'] = builds_path + end + + config.after(:suite) do + Dir.chdir(builds_path) do + `ls | grep -v .gitkeep | xargs rm -r` + end + end +end diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb index ef3a120d44a..f40ee862df8 100644 --- a/spec/support/stub_configuration.rb +++ b/spec/support/stub_configuration.rb @@ -17,8 +17,8 @@ module StubConfiguration allow(Gitlab.config.gravatar).to receive_messages(messages) end - def stub_reply_by_email_setting(messages) - allow(Gitlab.config.reply_by_email).to receive_messages(messages) + def stub_incoming_email_setting(messages) + allow(Gitlab.config.incoming_email).to receive_messages(messages) end private diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb new file mode 100644 index 00000000000..5e6744afda1 --- /dev/null +++ b/spec/support/stub_gitlab_calls.rb @@ -0,0 +1,77 @@ +module StubGitlabCalls + def stub_gitlab_calls + stub_session + stub_user + stub_project_8 + stub_project_8_hooks + stub_projects + stub_projects_owned + stub_ci_enable + end + + def stub_js_gitlab_calls + allow_any_instance_of(Network).to receive(:projects) { project_hash_array } + end + + private + + def gitlab_url + Gitlab.config.gitlab.url + end + + def stub_session + f = File.read(Rails.root.join('spec/support/gitlab_stubs/session.json')) + + stub_request(:post, "#{gitlab_url}api/v3/session.json"). + with(body: "{\"email\":\"test@test.com\",\"password\":\"123456\"}", + headers: { 'Content-Type'=>'application/json' }). + to_return(status: 201, body: f, headers: { 'Content-Type'=>'application/json' }) + end + + def stub_user + f = File.read(Rails.root.join('spec/support/gitlab_stubs/user.json')) + + stub_request(:get, "#{gitlab_url}api/v3/user?private_token=Wvjy2Krpb7y8xi93owUz"). + with(headers: { 'Content-Type'=>'application/json' }). + to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' }) + + stub_request(:get, "#{gitlab_url}api/v3/user?access_token=some_token"). + with(headers: { 'Content-Type'=>'application/json' }). + to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' }) + end + + def stub_project_8 + data = File.read(Rails.root.join('spec/support/gitlab_stubs/project_8.json')) + allow_any_instance_of(Network).to receive(:project).and_return(JSON.parse(data)) + end + + def stub_project_8_hooks + data = File.read(Rails.root.join('spec/support/gitlab_stubs/project_8_hooks.json')) + allow_any_instance_of(Network).to receive(:project_hooks).and_return(JSON.parse(data)) + end + + def stub_projects + f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json')) + + stub_request(:get, "#{gitlab_url}api/v3/projects.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz"). + with(headers: { 'Content-Type'=>'application/json' }). + to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' }) + end + + def stub_projects_owned + stub_request(:get, "#{gitlab_url}api/v3/projects/owned.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz"). + with(headers: { 'Content-Type'=>'application/json' }). + to_return(status: 200, body: "", headers: {}) + end + + def stub_ci_enable + stub_request(:put, "#{gitlab_url}api/v3/projects/2/services/gitlab-ci.json?private_token=Wvjy2Krpb7y8xi93owUz"). + with(headers: { 'Content-Type'=>'application/json' }). + to_return(status: 200, body: "", headers: {}) + end + + def project_hash_array + f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json')) + JSON.parse f + end +end diff --git a/spec/support/stub_gitlab_data.rb b/spec/support/stub_gitlab_data.rb new file mode 100644 index 00000000000..fa402f35b95 --- /dev/null +++ b/spec/support/stub_gitlab_data.rb @@ -0,0 +1,5 @@ +module StubGitlabData + def gitlab_ci_yaml + File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + end +end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 23f322e0a62..2e63e5f36af 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -16,7 +16,7 @@ describe 'gitlab:app namespace rake task' do end def reenable_backup_sub_tasks - %w{db repo uploads}.each do |subtask| + %w{db repo uploads builds}.each do |subtask| Rake::Task["gitlab:backup:#{subtask}:create"].reenable end end @@ -54,6 +54,7 @@ describe 'gitlab:app namespace rake task' do and_return({ gitlab_version: gitlab_version }) expect(Rake::Task["gitlab:backup:db:restore"]).to receive(:invoke) expect(Rake::Task["gitlab:backup:repo:restore"]).to receive(:invoke) + expect(Rake::Task["gitlab:backup:builds:restore"]).to receive(:invoke) expect(Rake::Task["gitlab:shell:setup"]).to receive(:invoke) expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error end @@ -111,18 +112,19 @@ describe 'gitlab:app namespace rake task' do it 'should set correct permissions on the tar contents' do tar_contents, exit_status = Gitlab::Popen.popen( - %W{tar -tvf #{@backup_tar} db uploads repositories} + %W{tar -tvf #{@backup_tar} db uploads repositories builds} ) expect(exit_status).to eq(0) expect(tar_contents).to match('db/') expect(tar_contents).to match('uploads/') expect(tar_contents).to match('repositories/') - expect(tar_contents).not_to match(/^.{4,9}[rwx].* (db|uploads|repositories)\/$/) + expect(tar_contents).to match('builds/') + expect(tar_contents).not_to match(/^.{4,9}[rwx].* (db|uploads|repositories|builds)\/$/) end it 'should delete temp directories' do temp_dirs = Dir.glob( - File.join(Gitlab.config.backup.path, '{db,repositories,uploads}') + File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds}') ) expect(temp_dirs).to be_empty @@ -158,11 +160,12 @@ describe 'gitlab:app namespace rake task' do it "does not contain skipped item" do tar_contents, exit_status = Gitlab::Popen.popen( - %W{tar -tvf #{@backup_tar} db uploads repositories} + %W{tar -tvf #{@backup_tar} db uploads repositories builds} ) expect(tar_contents).to match('db/') expect(tar_contents).to match('uploads/') + expect(tar_contents).to match('builds/') expect(tar_contents).not_to match('repositories/') end @@ -173,6 +176,7 @@ describe 'gitlab:app namespace rake task' do expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke expect(Rake::Task["gitlab:backup:repo:restore"]).not_to receive :invoke + expect(Rake::Task["gitlab:backup:builds:restore"]).to receive :invoke expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error end diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb new file mode 100644 index 00000000000..6b07fcfc987 --- /dev/null +++ b/spec/views/help/index.html.haml_spec.rb @@ -0,0 +1,41 @@ +require 'rails_helper' + +describe 'help/index' do + describe 'version information' do + it 'is hidden from guests' do + stub_user(nil) + stub_version('8.0.2', 'abcdefg') + stub_helpers + + render + + expect(rendered).not_to match '8.0.2' + expect(rendered).not_to match 'abcdefg' + end + + it 'is shown to users' do + stub_user + stub_version('8.0.2', 'abcdefg') + stub_helpers + + render + + expect(rendered).to match '8.0.2' + expect(rendered).to match 'abcdefg' + end + end + + def stub_user(user = double) + allow(view).to receive(:user_signed_in?).and_return(user) + end + + def stub_version(version, revision) + stub_const('Gitlab::VERSION', version) + stub_const('Gitlab::REVISION', revision) + end + + def stub_helpers + allow(view).to receive(:markdown).and_return('') + allow(view).to receive(:version_status_badge).and_return('') + end +end diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb index e8f1bd2fa2f..65a8d7d9197 100644 --- a/spec/workers/email_receiver_worker_spec.rb +++ b/spec/workers/email_receiver_worker_spec.rb @@ -5,7 +5,7 @@ describe EmailReceiverWorker do context "when reply by email is enabled" do before do - allow(Gitlab::ReplyByEmail).to receive(:enabled?).and_return(true) + allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(true) end it "calls the email receiver" do @@ -33,7 +33,7 @@ describe EmailReceiverWorker do context "when reply by email is disabled" do before do - allow(Gitlab::ReplyByEmail).to receive(:enabled?).and_return(false) + allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(false) end it "doesn't call the email receiver" do diff --git a/spec/workers/fork_registration_worker_spec.rb b/spec/workers/fork_registration_worker_spec.rb deleted file mode 100644 index cc6f574b29c..00000000000 --- a/spec/workers/fork_registration_worker_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ - -require 'spec_helper' - -describe ForkRegistrationWorker do - context "as a resque worker" do - it "reponds to #perform" do - expect(ForkRegistrationWorker.new).to respond_to(:perform) - end - end -end diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb new file mode 100644 index 00000000000..aa031106968 --- /dev/null +++ b/spec/workers/repository_fork_worker_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe RepositoryForkWorker do + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + + subject { RepositoryForkWorker.new } + + describe "#perform" do + it "creates a new repository from a fork" do + expect_any_instance_of(Gitlab::Shell).to receive(:fork_repository).with( + project.path_with_namespace, + fork_project.namespace.path). + and_return(true) + expect(ProjectCacheWorker).to receive(:perform_async) + + subject.perform(project.id, + project.path_with_namespace, + fork_project.namespace.path) + end + + it "handles bad fork" do + expect_any_instance_of(Gitlab::Shell).to receive(:fork_repository).and_return(false) + subject.perform(project.id, + project.path_with_namespace, + fork_project.namespace.path) + end + end +end |