diff options
291 files changed, 3164 insertions, 2222 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000000..1411a9194b5 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,39 @@ +before_script: + - export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin + - ruby -v + - which ruby + - gem install bundler + - which bundle + - echo $PATH + - cp config/database.yml.mysql config/database.yml + - cp config/gitlab.yml.example config/gitlab.yml + - ! 'sed "s/username\:.*$/username\: runner/" -i config/database.yml' + - ! 'sed "s/password\:.*$/password\: ''password''/" -i config/database.yml' + - sed "s/gitlabhq_test/gitlabhq_test_$((RANDOM/5000))/" -i config/database.yml + - touch log/application.log + - touch log/test.log + - bundle install --without postgres production --jobs $(nproc) + - bundle exec rake db:create RAILS_ENV=test +jobs: +- script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec + name: Rspec + runner: ruby,mysql +- script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach + name: Spinach + runner: ruby,mysql +- script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake jasmine:ci + name: Jasmine + runner: ruby,mysql +- script: + - bundle exec rubocop + name: Rubocop + runner: ruby,mysql +- script: + - bundle exec rake brakeman + name: Brakeman + runner: ruby,mysql +deploy_jobs: [] +skip_refs: '' diff --git a/CHANGELOG b/CHANGELOG index 8fbf85df338..9d558b15ab9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,17 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.12.0 (unreleased) + - Update oauth button logos for Twitter and Google to recommended assets + - Update browser gem to version 0.8.0 for IE11 support (Stan Hu) + - Fix timeout when rendering file with thousands of lines. + - Add "Remember me" checkbox to LDAP signin form. + - Don't notify users mentioned in code blocks or blockquotes. + - Omit link to generate labels if user does not have access to create them (Stan Hu) + - Show warning when a comment will add 10 or more people to the discussion. + - Disable changing of the source branch in merge request update API (Stan Hu) + - Shorten merge request WIP text. + - Add option to disallow users from registering any application to use GitLab as an OAuth provider + - Support editing target branch of merge request (Stan Hu) - Refactor permission checks with issues and merge requests project settings (Stan Hu) - Fix Markdown preview not working in Edit Milestone page (Stan Hu) - Fix Zen Mode not closing with ESC key (Stan Hu) @@ -8,11 +19,13 @@ v 7.12.0 (unreleased) - Add file attachment support in Milestone description (Stan Hu) - Fix milestone "Browse Issues" button. - Set milestone on new issue when creating issue from index with milestone filter active. + - Make namespace API available to all users (Stan Hu) - Add web hook support for note events (Stan Hu) - Disable "New Issue" and "New Merge Request" buttons when features are disabled in project settings (Stan Hu) - Remove Rack Attack monkey patches and bump to version 4.3.0 (Stan Hu) - Fix clone URL losing selection after a single click in Safari and Chrome (Stan Hu) - Fix git blame syntax highlighting when different commits break up lines (Stan Hu) + - Add "Resend confirmation e-mail" link in profile settings (Stan Hu) - Allow to configure location of the `.gitlab_shell_secret` file. (Jakub Jirutka) - Disabled expansion of top/bottom blobs for new file diffs - Update Asciidoctor gem to version 1.5.2. (Jakub Jirutka) @@ -29,6 +42,17 @@ v 7.12.0 (unreleased) - Clarify navigation labels for Project Settings and Group Settings. - Move user avatar and logout button to sidebar - You can not remove user if he/she is an only owner of group + - User should be able to leave group. If not - show him proper message + - User has ability to leave project + - Add SAML support as an omniauth provider + - Allow to configure a URL to show after sign out + - Add an option to automatically sign-in with an Omniauth provider + - Better performance for web editor (switched from satellites to rugged) + - GitLab CI service sends .gitlab-ci.yaml in each push call + - When remove project - move repository and schedule it removal + - Improve group removing logic + - Trigger create-hooks on backup restore task + - Add option to automatically link omniauth and LDAP identities v 7.11.4 - Fix missing bullets when creating lists @@ -36,6 +60,7 @@ v 7.11.4 v 7.11.3 - no changes + - Fix upgrader script (Martins Polakovs) v 7.11.2 - no changes @@ -46,6 +71,9 @@ v 7.11.1 v 7.11.0 - Fall back to Plaintext when Syntaxhighlighting doesn't work. Fixes some buggy lexers (Hannes Rosenögger) - Get editing comments to work in Chrome 43 again. + - Allow special character in users bio. I.e.: I <3 GitLab + +v 7.11.0 - Fix broken view when viewing history of a file that includes a path that used to be another file (Stan Hu) - Don't show duplicate deploy keys - Fix commit time being displayed in the wrong timezone in some cases (Hannes Rosenögger) @@ -117,6 +145,7 @@ v 7.10.4 - Fix DB error when trying to tag a repository (Stan Hu) - Fix Error 500 when searching Wiki pages (Stan Hu) - Unescape branch names in compare commit (Stan Hu) + - Order commit comments chronologically in API. v 7.10.2 - Fix CI links on MR page @@ -1,18 +1,7 @@ source "https://rubygems.org" -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.0" -# Make links from text -gem 'rails_autolink', '~> 1.1' - # Default values for AR models gem "default_value_for", "~> 3.0.0" @@ -31,6 +20,7 @@ gem 'omniauth-shibboleth' gem 'omniauth-kerberos', group: :kerberos gem 'omniauth-gitlab' gem 'omniauth-bitbucket' +gem 'omniauth-saml' gem 'doorkeeper', '2.1.3' gem "rack-oauth2", "~> 1.0.5" @@ -40,22 +30,30 @@ gem 'rqrcode-rails3' gem 'attr_encrypted', '1.3.4' # Browser detection -gem "browser" +gem "browser", '~> 0.8.0' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.1.13' +gem "gitlab_git", '~> 7.2.3' # Ruby/Rack Git Smart-HTTP Server Handler +# GitLab fork with a lot of changes (improved thread-safety, better memory usage etc) +# For full list of changes see https://github.com/SaitoWu/grack/compare/master...gitlabhq:master gem 'gitlab-grack', '~> 2.0.2', require: 'grack' # 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" # Git Wiki gem 'gollum-lib', '~> 4.0.2' # Language detection +# GitLab fork of linguist does not require pygments/python dependency. +# New version of original gem also dropped pygments support but it has strict +# dependency to unstable rugged version. We have internal issue for replacing +# fork with original gem when we meet on same rugged version - https://dev.gitlab.org/gitlab/gitlabhq/issues/2052. gem "gitlab-linguist", "~> 3.0.1", require: "linguist" # API @@ -83,7 +81,7 @@ gem "carrierwave" gem 'dropzonejs-rails' # for aws storage -gem "fog", "~> 1.14" +gem "fog", "~> 1.25.0" gem "unf" # Authorization @@ -186,23 +184,23 @@ gem 'charlock_holmes' gem "sass-rails", '~> 4.0.2' gem "coffee-rails" gem "uglifier" -gem 'turbolinks' +gem 'turbolinks', '~> 2.5.0' gem 'jquery-turbolinks' -gem 'select2-rails' +gem 'addressable' +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" -gem "jquery-ui-rails" -gem "jquery-scrollto-rails" -gem "raphael-rails", "~> 2.1.2" -gem 'bootstrap-sass', '~> 3.0' -gem "font-awesome-rails", '~> 4.2' -gem "gitlab_emoji", "~> 0.1" -gem "gon", '~> 5.0.0' +gem 'jquery-rails', '3.1.2' +gem 'jquery-scrollto-rails' +gem 'jquery-ui-rails' gem 'nprogress-rails' +gem 'raphael-rails', '~> 2.1.2' gem 'request_store' -gem "virtus" -gem 'addressable' +gem 'select2-rails' +gem 'virtus' group :development do gem 'brakeman', require: false @@ -241,32 +239,25 @@ group :development, :test do # Generate Fake data gem 'ffaker', '~> 2.0.0' - # Guard - gem 'guard-rspec' - gem 'guard-spinach' - - # Notification - gem 'rb-fsevent', require: darwin_only('rb-fsevent') - gem 'growl', require: darwin_only('growl') - gem 'rb-inotify', require: linux_only('rb-inotify') - # PhantomJS driver for Capybara gem 'poltergeist', '~> 1.5.1' - gem 'jasmine-rails' + gem 'teaspoon', '~> 1.0.0' + gem 'teaspoon-jasmine' - gem "spring", '~> 1.3.1' - gem "spring-commands-rspec", '1.0.4' - gem "spring-commands-spinach", '1.0.0' + gem 'spring', '~> 1.3.1' + gem 'spring-commands-rspec', '~> 1.0.0' + gem 'spring-commands-spinach', '~> 1.0.0' + gem 'spring-commands-teaspoon', '~> 0.0.2' gem "byebug" end group :test do - gem "simplecov", require: false - gem "shoulda-matchers", "~> 2.7.0" + gem 'simplecov', require: false + gem 'shoulda-matchers', '~> 2.8.0', require: false gem 'email_spec' - gem "webmock" + gem 'webmock', '~> 1.21.0' gem 'test_after_commit' end diff --git a/Gemfile.lock b/Gemfile.lock index 7dbc3b4ffa9..1de29ad8f8a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,7 @@ GEM remote: https://rubygems.org/ specs: + CFPropertyList (2.3.1) RedCloth (4.2.9) ace-rails-ap (2.0.1) actionmailer (4.1.9) @@ -35,7 +36,7 @@ GEM tzinfo (~> 1.1) acts-as-taggable-on (3.5.0) activerecord (>= 3.2, < 5) - addressable (2.3.5) + addressable (2.3.8) annotate (2.6.0) activerecord (>= 2.3.0) rake (>= 0.8.7) @@ -75,7 +76,7 @@ GEM ruby_parser (~> 3.5.0) sass (~> 3.0) terminal-table (~> 1.4) - browser (0.7.2) + browser (0.8.0) builder (3.2.2) byebug (3.2.0) columnize (~> 0.8) @@ -101,13 +102,13 @@ GEM coderay (1.1.0) coercible (1.0.0) descendants_tracker (~> 0.0.1) - coffee-rails (4.0.1) + coffee-rails (4.1.0) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.0) - coffee-script (2.2.0) + coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.6.3) + coffee-script-source (1.9.1.1) colored (1.2) colorize (0.5.8) columnize (0.9.0) @@ -118,8 +119,8 @@ GEM simplecov (>= 0.7) term-ansicolor thor - crack (0.4.1) - safe_yaml (~> 0.9.0) + crack (0.4.2) + safe_yaml (~> 1.0.0) creole (0.3.8) d3_rails (3.5.5) railties (>= 3.1.0) @@ -163,7 +164,7 @@ GEM erubis (2.7.0) escape_utils (0.2.4) eventmachine (1.0.4) - excon (0.32.1) + excon (0.45.3) execjs (2.5.2) expression_parser (0.9.0) factory_girl (4.3.0) @@ -178,29 +179,69 @@ GEM fastercsv (1.5.5) ffaker (2.0.0) ffi (1.9.8) - fog (1.21.0) - fog-brightbox - fog-core (~> 1.21, >= 1.21.1) + fission (0.5.0) + CFPropertyList (~> 2.2) + fog (1.25.0) + fog-brightbox (~> 0.4) + fog-core (~> 1.25) fog-json + fog-profitbricks + fog-radosgw (>= 0.0.2) + fog-sakuracloud (>= 0.0.4) + fog-softlayer + fog-terremark + fog-vmfusion + fog-voxel + fog-xml (~> 0.1.1) + ipaddress (~> 0.5) nokogiri (~> 1.5, >= 1.5.11) - fog-brightbox (0.0.1) - fog-core + opennebula + fog-brightbox (0.7.1) + fog-core (~> 1.22) fog-json - fog-core (1.21.1) + inflecto (~> 0.0.2) + fog-core (1.30.0) builder - excon (~> 0.32) - formatador (~> 0.2.0) + excon (~> 0.45) + formatador (~> 0.2) mime-types net-scp (~> 1.1) net-ssh (>= 2.1.3) - fog-json (1.0.0) - multi_json (~> 1.0) + fog-json (1.0.2) + fog-core (~> 1.0) + multi_json (~> 1.10) + fog-profitbricks (0.0.3) + fog-core + fog-xml + nokogiri + fog-radosgw (0.0.4) + fog-core (>= 1.21.0) + fog-json + fog-xml (>= 0.0.1) + fog-sakuracloud (1.0.1) + fog-core + fog-json + fog-softlayer (0.4.6) + fog-core + fog-json + fog-terremark (0.1.0) + fog-core + fog-xml + fog-vmfusion (0.1.0) + fission + fog-core + fog-voxel (0.1.0) + fog-core + fog-xml + fog-xml (0.1.2) + fog-core + nokogiri (~> 1.5, >= 1.5.11) font-awesome-rails (4.2.0.0) railties (>= 3.2, < 5.0) foreman (0.63.0) dotenv (>= 0.7) thor (>= 0.13.6) - formatador (0.2.4) + formatador (0.2.5) gemnasium-gitlab-service (0.2.6) rugged (~> 0.21) gemojione (2.0.0) @@ -225,7 +266,7 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.1.0) gemojione (~> 2.0) - gitlab_git (7.1.13) + gitlab_git (7.2.3) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) @@ -261,19 +302,6 @@ GEM grape-entity (0.4.2) activesupport multi_json (>= 1.3.2) - growl (1.0.3) - guard (2.2.4) - formatador (>= 0.2.4) - listen (~> 2.1) - lumberjack (~> 1.0) - pry (>= 0.9.12) - thor (>= 0.18.1) - guard-rspec (4.2.0) - guard (>= 2.1.1) - rspec (>= 2.14, < 4.0) - guard-spinach (0.0.2) - guard (>= 1.1) - spinach haml (4.0.5) tilt haml-rails (0.5.3) @@ -300,14 +328,10 @@ GEM i18n (0.7.0) ice_cube (0.11.1) ice_nine (0.10.0) - jasmine-core (2.2.0) - jasmine-rails (0.10.8) - jasmine-core (>= 1.3, < 3.0) - phantomjs (>= 1.9) - railties (>= 3.2.0) - sprockets-rails + inflecto (0.0.2) + ipaddress (0.8.0) jquery-atwho-rails (1.0.1) - jquery-rails (3.1.0) + jquery-rails (3.1.2) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) jquery-scrollto-rails (1.4.3) @@ -332,7 +356,8 @@ GEM celluloid (~> 0.16.0) rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) - lumberjack (1.0.4) + macaddr (1.7.1) + systemu (~> 2.6.2) mail (2.6.3) mime-types (>= 1.16, < 3) method_source (0.8.2) @@ -346,9 +371,9 @@ GEM multipart-post (1.2.0) mysql2 (0.3.16) net-ldap (0.11) - net-scp (1.1.2) + net-scp (1.2.1) net-ssh (>= 2.6.5) - net-ssh (2.8.0) + net-ssh (2.9.2) newrelic_rpm (3.9.4.245) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) @@ -389,18 +414,24 @@ GEM omniauth-oauth2 (1.1.1) oauth2 (~> 0.8.0) omniauth (~> 1.0) + omniauth-saml (1.3.1) + omniauth (~> 1.1) + ruby-saml (~> 0.8.1) omniauth-shibboleth (1.1.1) omniauth (>= 1.0.0) omniauth-twitter (1.0.1) multi_json (~> 1.3) omniauth-oauth (~> 1.0) + opennebula (4.12.1) + json + nokogiri + rbvmomi org-ruby (0.9.12) rubypants (~> 0.2) orm_adapter (0.5.0) parser (2.2.0.2) ast (>= 1.1, < 3.0) pg (0.15.1) - phantomjs (1.9.8.0) poltergeist (1.5.1) capybara (~> 2.1) cliver (~> 0.3.1) @@ -418,7 +449,7 @@ GEM quiet_assets (1.0.2) railties (>= 3.1, < 5.0) racc (1.4.10) - rack (1.5.2) + rack (1.5.3) rack-accept (0.4.5) rack (>= 0.4) rack-attack (4.3.0) @@ -450,8 +481,6 @@ GEM sprockets-rails (~> 2.0) rails-observers (0.1.2) activemodel (~> 4.0) - rails_autolink (1.1.6) - rails (> 3.1) railties (4.1.9) actionpack (= 4.1.9) activesupport (= 4.1.9) @@ -464,6 +493,10 @@ GEM rb-fsevent (0.9.4) rb-inotify (0.9.5) ffi (>= 0.5.0) + rbvmomi (1.8.2) + builder + nokogiri (>= 1.4.1) + trollop rdoc (3.12.2) json (~> 1.4) redcarpet (3.2.3) @@ -497,10 +530,6 @@ GEM rqrcode (0.4.2) rqrcode-rails3 (0.1.7) rqrcode (>= 0.4.2) - rspec (2.99.0) - rspec-core (~> 2.99.0) - rspec-expectations (~> 2.99.0) - rspec-mocks (~> 2.99.0) rspec-collection_matchers (1.1.2) rspec-expectations (>= 2.99.0.beta1) rspec-core (2.99.2) @@ -523,6 +552,9 @@ GEM rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.4) ruby-progressbar (1.7.1) + ruby-saml (0.8.2) + nokogiri (>= 1.5.0) + uuid (~> 2.3) ruby2ruby (2.1.3) ruby_parser (~> 3.1) sexp_processor (~> 4.0) @@ -532,7 +564,7 @@ GEM rubypants (0.2.0) rugged (0.22.2) rugments (1.0.0.beta7) - safe_yaml (0.9.7) + safe_yaml (1.0.4) sanitize (2.1.0) nokogiri (>= 1.4.4) sass (3.2.19) @@ -554,7 +586,7 @@ GEM thor (~> 0.14) settingslogic (2.0.9) sexp_processor (4.4.5) - shoulda-matchers (2.7.0) + shoulda-matchers (2.8.0) activesupport (>= 3.0.0) sidekiq (3.3.0) celluloid (>= 0.16.0) @@ -594,6 +626,8 @@ GEM spring (>= 0.9.1) spring-commands-spinach (1.0.0) spring (>= 0.9.1) + spring-commands-teaspoon (0.0.2) + spring (>= 0.9.1) sprockets (2.11.0) hike (~> 1.2) multi_json (~> 1.0) @@ -606,8 +640,13 @@ GEM stamp (0.5.0) state_machine (1.2.0) stringex (2.5.2) + systemu (2.6.5) task_list (1.0.2) html-pipeline + teaspoon (1.0.2) + railties (>= 3.2.5, < 5) + teaspoon-jasmine (2.2.0) + teaspoon (>= 1.0.0) temple (0.6.7) term-ansicolor (1.2.2) tins (~> 0.8) @@ -633,7 +672,8 @@ GEM multi_json (~> 1.7) twitter-stream (~> 0.1) tins (0.13.1) - turbolinks (2.0.0) + trollop (2.1.2) + turbolinks (2.5.3) coffee-rails twitter-stream (0.1.16) eventmachine (>= 0.12.8) @@ -654,6 +694,8 @@ GEM raindrops (~> 0.7) unicorn-worker-killer (0.4.2) unicorn (~> 4) + uuid (2.3.7) + macaddr (~> 1.0) version_sorter (2.0.0) virtus (1.0.1) axiom-types (~> 0.0.5) @@ -662,8 +704,8 @@ GEM equalizer (~> 0.0.7) warden (1.2.3) rack (>= 1.0) - webmock (1.16.0) - addressable (>= 2.2.7) + webmock (1.21.0) + addressable (>= 2.3.6) crack (>= 0.3.2) websocket-driver (0.3.3) wikicloth (0.8.1) @@ -690,7 +732,7 @@ DEPENDENCIES binding_of_caller bootstrap-sass (~> 3.0) brakeman - browser + browser (~> 0.8.0) byebug cal-heatmap-rails (~> 0.0.1) capybara (~> 2.2.1) @@ -714,7 +756,7 @@ DEPENDENCIES enumerize factory_girl_rails ffaker (~> 2.0.0) - fog (~> 1.14) + fog (~> 1.25.0) font-awesome-rails (~> 4.2) foreman gemnasium-gitlab-service (~> 0.2) @@ -723,23 +765,19 @@ DEPENDENCIES gitlab-grack (~> 2.0.2) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) - gitlab_git (~> 7.1.13) + gitlab_git (~> 7.2.3) gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.1) gollum-lib (~> 4.0.2) gon (~> 5.0.0) grape (~> 0.6.1) grape-entity (~> 0.4.2) - growl - guard-rspec - guard-spinach haml-rails hipchat (~> 1.5.0) html-pipeline (~> 1.11.0) httparty - jasmine-rails jquery-atwho-rails (~> 1.0.0) - jquery-rails + jquery-rails (= 3.1.2) jquery-scrollto-rails jquery-turbolinks jquery-ui-rails @@ -757,6 +795,7 @@ DEPENDENCIES omniauth-gitlab omniauth-google-oauth2 omniauth-kerberos + omniauth-saml omniauth-shibboleth omniauth-twitter org-ruby (= 0.9.12) @@ -769,10 +808,7 @@ DEPENDENCIES rack-mini-profiler rack-oauth2 (~> 1.0.5) rails (~> 4.1.0) - rails_autolink (~> 1.1) raphael-rails (~> 2.1.2) - rb-fsevent - rb-inotify rdoc (~> 3.6) redcarpet (~> 3.2.3) redis-rails @@ -788,7 +824,7 @@ DEPENDENCIES seed-fu select2-rails settingslogic - shoulda-matchers (~> 2.7.0) + shoulda-matchers (~> 2.8.0) sidekiq (~> 3.3) sidetiq (= 0.6.3) simplecov @@ -798,15 +834,18 @@ DEPENDENCIES slim spinach-rails spring (~> 1.3.1) - spring-commands-rspec (= 1.0.4) - spring-commands-spinach (= 1.0.0) + spring-commands-rspec (~> 1.0.0) + spring-commands-spinach (~> 1.0.0) + spring-commands-teaspoon (~> 0.0.2) stamp state_machine task_list (= 1.0.2) + teaspoon (~> 1.0.0) + teaspoon-jasmine test_after_commit thin tinder (~> 1.9.2) - turbolinks + turbolinks (~> 2.5.0) uglifier underscore-rails (~> 1.4.4) unf @@ -814,5 +853,5 @@ DEPENDENCIES unicorn-worker-killer version_sorter virtus - webmock + webmock (~> 1.21.0) wikicloth (= 0.8.1) diff --git a/Guardfile b/Guardfile deleted file mode 100644 index 68ac3232b09..00000000000 --- a/Guardfile +++ /dev/null @@ -1,27 +0,0 @@ -# A sample Guardfile -# More info at https://github.com/guard/guard#readme - -guard 'rspec', cmd: "spring rspec", all_on_start: false, all_after_pass: false do - watch(%r{^spec/.+_spec\.rb$}) - watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } - watch(%r{^lib/api/(.+)\.rb$}) { |m| "spec/requests/api/#{m[1]}_spec.rb" } - watch('spec/spec_helper.rb') { "spec" } - - # Rails example - watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } - watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } - watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } - watch(%r{^spec/support/(.+)\.rb$}) { "spec" } - watch('config/routes.rb') { "spec/routing" } - watch('app/controllers/application_controller.rb') { "spec/controllers" } - - # Capybara request specs - watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } -end - -guard 'spinach', command_prefix: 'spring' do - watch(%r|^features/(.*)\.feature|) - watch(%r|^features/steps/(.*)([^/]+)\.rb|) do |m| - "features/#{m[1]}#{m[2]}.feature" - end -end diff --git a/app/assets/images/authbuttons/google_64.png b/app/assets/images/authbuttons/google_64.png Binary files differindex 94a0e089c6e..fb64f8bee68 100644 --- a/app/assets/images/authbuttons/google_64.png +++ b/app/assets/images/authbuttons/google_64.png diff --git a/app/assets/images/authbuttons/twitter_64.png b/app/assets/images/authbuttons/twitter_64.png Binary files differindex 5c9f14cb077..e3bd9169a34 100644 --- a/app/assets/images/authbuttons/twitter_64.png +++ b/app/assets/images/authbuttons/twitter_64.png diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index ea2a4b97101..6a3f7386d5b 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -49,8 +49,6 @@ window.slugify = (text) -> window.ajaxGet = (url) -> $.ajax({type: "GET", url: url, dataType: "script"}) -window.showAndHide = (selector) -> - window.split = (val) -> return val.split( /,\s*/ ) @@ -92,15 +90,7 @@ window.disableButtonIfAnyEmptyField = (form, form_selector, button_selector) -> window.sanitize = (str) -> return str.replace(/<(?:.|\n)*?>/gm, '') -window.linkify = (str) -> - exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig - return str.replace(exp,"<a href='$1'>$1</a>") - -window.simpleFormat = (str) -> - linkify(sanitize(str).replace(/\n/g, '<br />')) - window.unbindEvents = -> - $(document).unbind('scroll') $(document).off('scroll') window.shiftWindow = -> @@ -177,6 +167,10 @@ $ -> $(@).next('table').show() $(@).remove() + $('.navbar-toggle').on 'click', -> + $('.header-content .title').toggle() + $('.header-content .navbar-collapse').toggle() + # Show/hide comments on diff $("body").on "click", ".js-toggle-diff-comments", (e) -> $(@).toggleClass('active') @@ -192,14 +186,3 @@ $ -> new ConfirmDangerModal(form, text) new Aside() - -(($) -> - # Disable an element and add the 'disabled' Bootstrap class - $.fn.extend disable: -> - $(@).attr('disabled', 'disabled').addClass('disabled') - - # Enable an element and remove the 'disabled' Bootstrap class - $.fn.extend enable: -> - $(@).removeAttr('disabled').removeClass('disabled') - -)(jQuery) diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee index fca2a290e2d..a7476146010 100644 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ b/app/assets/javascripts/dropzone_input.js.coffee @@ -10,12 +10,17 @@ class @DropzoneInput iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>" btnAlert = "<button type=\"button\"" + alertAttr + ">×</button>" project_uploads_path = window.project_uploads_path or null + markdown_preview_path = window.markdown_preview_path or null max_file_size = gon.max_file_size or 10 form_textarea = $(form).find("textarea.markdown-area") form_textarea.wrap "<div class=\"div-dropzone\"></div>" - form_textarea.bind 'paste', (event) => + form_textarea.on 'paste', (event) => handlePaste(event) + form_textarea.on "input", -> + hideReferencedUsers() + form_textarea.on "blur", -> + renderMarkdown() form_dropzone = $(form).find('.div-dropzone') form_dropzone.parent().addClass "div-dropzone-wrapper" @@ -45,16 +50,7 @@ class @DropzoneInput form.find(".md-write-holder").hide() form.find(".md-preview-holder").show() - preview = form.find(".js-md-preview") - mdText = form.find(".markdown-area").val() - if mdText.trim().length is 0 - preview.text "Nothing to preview." - else - preview.text "Loading..." - $.post($(this).data("url"), - md_text: mdText - ).success (previewData) -> - preview.html previewData + renderMarkdown() # Write button $(document).off "click", ".js-md-write-button" @@ -133,6 +129,40 @@ class @DropzoneInput child = $(dropzone[0]).children("textarea") + hideReferencedUsers = -> + referencedUsers = form.find(".referenced-users") + referencedUsers.hide() + + renderReferencedUsers = (users) -> + referencedUsers = form.find(".referenced-users") + + if referencedUsers.length + if users.length >= 10 + referencedUsers.show() + referencedUsers.find(".js-referenced-users-count").text users.length + else + referencedUsers.hide() + + renderMarkdown = -> + preview = form.find(".js-md-preview") + mdText = form.find(".markdown-area").val() + if mdText.trim().length is 0 + preview.text "Nothing to preview." + hideReferencedUsers() + else + preview.text "Loading..." + $.ajax( + type: "POST", + url: markdown_preview_path, + data: { + text: mdText + }, + dataType: "json" + ).success (data) -> + preview.html data.body + + renderReferencedUsers data.references.users + formatLink = (link) -> text = "[#{link.alt}](#{link.url})" text = "!#{text}" if link.is_image diff --git a/app/assets/javascripts/extensions/jquery.js.coffee b/app/assets/javascripts/extensions/jquery.js.coffee index 40fb6cb9fc3..0a9db8eb5ef 100644 --- a/app/assets/javascripts/extensions/jquery.js.coffee +++ b/app/assets/javascripts/extensions/jquery.js.coffee @@ -1,13 +1,11 @@ -$.fn.showAndHide = -> - $(@).show(). - delay(3000). - fadeOut() - -$.fn.enableButton = -> - $(@).removeAttr('disabled'). - removeClass('disabled') - -$.fn.disableButton = -> - $(@).attr('disabled', 'disabled'). - addClass('disabled') +# Disable an element and add the 'disabled' Bootstrap class +$.fn.extend disable: -> + $(@) + .attr('disabled', 'disabled') + .addClass('disabled') +# Enable an element and remove the 'disabled' Bootstrap class +$.fn.extend enable: -> + $(@) + .removeAttr('disabled') + .removeClass('disabled') diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee index 4eb3f3c03f3..7967892f856 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ b/app/assets/javascripts/gfm_auto_complete.js.coffee @@ -10,7 +10,7 @@ GitLab.GfmAutoComplete = # Team Members Members: - template: '<li>${username} <small>${name}</small></li>' + template: '<li>${username} <small>${title}</small></li>' # Issues and MergeRequests Issues: @@ -34,7 +34,13 @@ GitLab.GfmAutoComplete = searchKey: 'search' callbacks: beforeSave: (members) -> - $.map members, (m) -> name: m.name, username: m.username, search: "#{m.username} #{m.name}" + $.map members, (m) -> + title = m.name + title += " (#{m.count})" if m.count + + username: m.username + title: sanitize(title) + search: sanitize("#{m.username} #{m.name}") input.atwho at: '#' @@ -44,7 +50,10 @@ GitLab.GfmAutoComplete = insertTpl: '${atwho-at}${id}' callbacks: beforeSave: (issues) -> - $.map issues, (i) -> id: i.iid, title: sanitize(i.title), search: "#{i.iid} #{i.title}" + $.map issues, (i) -> + id: i.iid + title: sanitize(i.title) + search: "#{i.iid} #{i.title}" input.atwho at: '!' @@ -54,7 +63,10 @@ GitLab.GfmAutoComplete = insertTpl: '${atwho-at}${id}' callbacks: beforeSave: (merges) -> - $.map merges, (m) -> id: m.iid, title: sanitize(m.title), search: "#{m.iid} #{m.title}" + $.map merges, (m) -> + id: m.iid + title: sanitize(m.title) + search: "#{m.iid} #{m.title}" input.one 'focus', => $.getJSON(@dataSource).done (data) -> diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index 86ad3d03bac..74d6b80be5e 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -1,4 +1,3 @@ -#= require jquery #= require jquery.waitforimages #= require task_list diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 3937c428e24..b8f916b5223 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -1,5 +1,4 @@ -#= require jquery -#= require bootstrap +#= require jquery.waitforimages #= require task_list class @MergeRequest @@ -26,7 +25,7 @@ class @MergeRequest @commits_loaded = @opts.commits_loaded or false this.bindEvents() - this.activateTabFromHash() + this.activateTabFromPath() this.initMergeWidget() this.$('.show-all-commits').on 'click', => @@ -82,19 +81,16 @@ class @MergeRequest bindEvents: -> this.$('.merge-request-tabs a[data-toggle="tab"]').on 'shown.bs.tab', (e) => $target = $(e.target) - - # Nothing else to be done if we're on the first tab - return if $target.data('action') == 'notes' - - # Persist current tab selection via URL - href = $target.attr('href') - if href.substr(0,1) == '#' - location.replace("#!#{href.substr(1)}") + tab_action = $target.data('action') # Lazy-load diffs - if $target.data('action') == 'diffs' + if tab_action == 'diffs' this.loadDiff() unless @diffs_loaded - $('.diff-header').trigger("sticky_kit:recalc") + $('.diff-header').trigger('sticky_kit:recalc') + + # Skip tab-persisting behavior on MergeRequests#new + unless @opts.action == 'new' + @setCurrentAction(tab_action) this.$('.accept_merge_request').on 'click', -> $('.automerge_widget.can_be_merged').hide() @@ -112,27 +108,54 @@ class @MergeRequest this.$('.remove_source_branch_in_progress').hide() this.$('.remove_source_branch_widget.failed').show() - # Activates a tab section based on the `#!` URL hash + # Activate a tab based on the current URL path # - # If no hash value is present (i.e., on the initial page load), the first tab - # is selected by default. + # If the current action is 'show' or 'new' (i.e., initial page load), + # activates the first tab, otherwise activates the tab corresponding to the + # current action (diffs, commits). + activateTabFromPath: -> + if @opts.action == 'show' || @opts.action == 'new' + this.$('.merge-request-tabs a[data-toggle="tab"]:first').tab('show') + else + this.$(".merge-request-tabs a[data-action='#{@opts.action}']").tab('show') + + # Replaces the current Merge Request-specific action in the URL with a new one # - # ... unless the current controller action is `diffs`, in which case that tab - # is selected instead. Fun, right? + # If the action is "notes", the URL is reset to the standard + # `MergeRequests#show` route. # - # Note: We use a `#!` instead of a standard URL hash for two reasons: + # Examples: # - # 1. Prevents the hash acting like an anchor and scrolling the page. - # 2. Prevents mutating browser history. - activateTabFromHash: -> - # Correct the hash if we came here directly via the `/diffs` path - if location.hash == '' and @opts.action == 'diffs' - location.replace('#!diffs') - - if location.hash == '' - this.$('.merge-request-tabs a[data-toggle="tab"]:first').tab('show') - else if location.hash.substr(0,2) == '#!' - this.$(".merge-request-tabs a[href='##{location.hash.substr(2)}']").tab("show") + # location.pathname # => "/namespace/project/merge_requests/1" + # setCurrentAction('diffs') + # location.pathname # => "/namespace/project/merge_requests/1/diffs" + # + # location.pathname # => "/namespace/project/merge_requests/1/diffs" + # setCurrentAction('notes') + # location.pathname # => "/namespace/project/merge_requests/1" + # + # location.pathname # => "/namespace/project/merge_requests/1/diffs" + # setCurrentAction('commits') + # location.pathname # => "/namespace/project/merge_requests/1/commits" + setCurrentAction: (action) -> + # Normalize action, just to be safe + action = 'notes' if action == 'show' + + # Remove a trailing '/commits' or '/diffs' + new_state = location.pathname.replace(/\/(commits|diffs)\/?$/, '') + + # Append the new action if we're on a tab other than 'notes' + unless action == 'notes' + new_state += "/#{action}" + + # Ensure parameters and hash come along for the ride + new_state += location.search + location.hash + + # Replace the current history state with the new one without breaking + # Turbolinks' history. + # + # See https://github.com/rails/turbolinks/issues/363 + history.replaceState {turbolinks: true, url: new_state}, '', new_state showState: (state) -> $('.automerge_widget').hide() @@ -161,7 +184,7 @@ class @MergeRequest loadDiff: (event) -> $.ajax type: 'GET' - url: this.$('.merge-request-tabs .diffs-tab a').data('source') + ".json" + url: this.$('.merge-request-tabs .diffs-tab a').attr('href') + ".json" beforeSend: => this.$('.mr-loading-status .loading').show() complete: => diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index f186fec2a0c..21656f59149 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -1,6 +1,4 @@ -#= require jquery #= require autosave -#= require bootstrap #= require dropzone #= require dropzone_input #= require gfm_auto_complete @@ -65,13 +63,11 @@ class @Notes # fetch notes when tab becomes visible $(document).on "visibilitychange", @visibilityChange - @notes_forms = '.js-main-target-form textarea, .js-discussion-note-form textarea' # Chrome doesn't fire keypress or keyup for Command+Enter, so we need keydown. - $(document).on('keydown', @notes_forms, (e) -> + $(document).on 'keydown', '.js-note-text', (e) -> return if e.originalEvent.repeat if e.keyCode == 10 || ((e.metaKey || e.ctrlKey) && e.keyCode == 13) - $(@).parents('form').submit() - ) + $(@).closest('form').submit() cleanBinding: -> $(document).off "ajax:success", ".js-main-target-form" @@ -86,7 +82,7 @@ class @Notes $(document).off "click", ".js-discussion-reply-button" $(document).off "click", ".js-add-diff-note-button" $(document).off "visibilitychange" - $(document).off "keydown", @notes_forms + $(document).off "keydown", ".js-note-text" $(document).off "keyup", ".js-note-text" $(document).off "click", ".js-note-target-reopen" $(document).off "click", ".js-note-target-close" diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index de356fbec77..40459a9a155 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile.js.coffee @@ -12,11 +12,11 @@ class @Profile $(this).find('.update-failed').hide() $('.update-username form').on 'ajax:complete', -> - $(this).find('.btn-save').enableButton() + $(this).find('.btn-save').enable() $(this).find('.loading-gif').hide() $('.update-notifications').on 'ajax:complete', -> - $(this).find('.btn-save').enableButton() + $(this).find('.btn-save').enable() $('.js-choose-user-avatar-button').bind "click", -> diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee index 6b534f29218..bb532194682 100644 --- a/app/assets/javascripts/shortcuts_issuable.coffee +++ b/app/assets/javascripts/shortcuts_issuable.coffee @@ -1,6 +1,4 @@ -#= require jquery #= require mousetrap - #= require shortcuts_navigation class @ShortcutsIssuable extends ShortcutsNavigation diff --git a/app/assets/javascripts/stat_graph_contributors.js.coffee b/app/assets/javascripts/stat_graph_contributors.js.coffee index ed12bdcef22..3be14cb43dd 100644 --- a/app/assets/javascripts/stat_graph_contributors.js.coffee +++ b/app/assets/javascripts/stat_graph_contributors.js.coffee @@ -1,5 +1,4 @@ #= require d3 -#= require jquery #= require stat_graph_contributors_util class @ContributorsStatGraph diff --git a/app/assets/javascripts/zen_mode.js.coffee b/app/assets/javascripts/zen_mode.js.coffee index dc6a84c6c52..8a0564a9098 100644 --- a/app/assets/javascripts/zen_mode.js.coffee +++ b/app/assets/javascripts/zen_mode.js.coffee @@ -1,3 +1,7 @@ +#= require dropzone +#= require mousetrap +#= require mousetrap/pause + class @ZenMode constructor: -> @active_zen_area = null @@ -26,7 +30,7 @@ class @ZenMode @exitZenMode() $(document).on 'keydown', (e) => - if e.keyCode is $.ui.keyCode.ESCAPE + if e.keyCode is 27 # Esc @exitZenMode() e.preventDefault() @@ -42,7 +46,9 @@ class @ZenMode @active_checkbox.prop('checked', false) @active_zen_area = null @active_checkbox = null - window.location.hash = '' - window.scrollTo(window.pageXOffset, @scroll_position) + @restoreScroll(@scroll_position) # Enable dropzone when leaving ZEN mode Dropzone.forElement('.div-dropzone').enable() + + restoreScroll: (y) -> + window.scrollTo(window.pageXOffset, y) diff --git a/app/assets/stylesheets/base/layout.scss b/app/assets/stylesheets/base/layout.scss index 62c11b06368..690d89a5c16 100644 --- a/app/assets/stylesheets/base/layout.scss +++ b/app/assets/stylesheets/base/layout.scss @@ -4,7 +4,7 @@ html { &.touch .tooltip { display: none !important; } body { - padding-top: 46px; + padding-top: $header-height; } } diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss index 596376c3970..08f153dfbc9 100644 --- a/app/assets/stylesheets/base/variables.scss +++ b/app/assets/stylesheets/base/variables.scss @@ -1,16 +1,19 @@ $style_color: #474D57; -$hover: #FFF3EB; +$hover: #FFFAF1; $gl-text-color: #222222; $gl-link-color: #446e9b; $nprogress-color: #c0392b; $gl-font-size: 14px; $list-font-size: 15px; +$sidebar_collapsed_width: 52px; $sidebar_width: 230px; $avatar_radius: 50%; $code_font_size: 13px; $code_line_height: 1.5; $border-color: #E5E5E5; $background-color: #f5f5f5; +$header-height: 50px; + /* * State colors: diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index b69c5c4b574..1419a9cded9 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -307,7 +307,7 @@ table { } .btn-sign-in { - margin-top: 5px; + margin-top: 7px; text-shadow: none; } diff --git a/app/assets/stylesheets/generic/forms.scss b/app/assets/stylesheets/generic/forms.scss index 7e070b4f386..4282832e2bf 100644 --- a/app/assets/stylesheets/generic/forms.scss +++ b/app/assets/stylesheets/generic/forms.scss @@ -49,14 +49,6 @@ label { width: 250px; } -.input-mx-250 { - max-width: 250px; -} - -.input-mn-300 { - min-width: 300px; -} - .custom-form-control { width: 150px; } diff --git a/app/assets/stylesheets/generic/header.scss b/app/assets/stylesheets/generic/header.scss index fe32b024f49..5e8701830e7 100644 --- a/app/assets/stylesheets/generic/header.scss +++ b/app/assets/stylesheets/generic/header.scss @@ -3,76 +3,40 @@ * */ header { + &.navbar-empty { + background: #FFF; + border-bottom: 1px solid #EEE; + + .center-logo { + margin: 8px 0; + text-align: center; + } + } + &.navbar-gitlab { z-index: 100; margin-bottom: 0; - min-height: 40px; + min-height: $header-height; border: none; width: 100%; .container { + background: #FFF; width: 100% !important; padding: 0; - padding-right: 35px; - background: #FFF; - border-bottom: 1px solid #EEE; filter: none; - .title { - position: relative; - float: left; - margin: 0; - margin-left: 25px; - font-size: 18px; - line-height: 44px; - font-weight: bold; - color: #444; - - @include str-truncated(37%); - - a { - color: #444; - &:hover { - text-decoration: underline; - } - } - } - - .app_logo { - border-bottom: 1px solid transparent; - margin-bottom: -1px; - - a { - padding: 5px 8px; - - img { - float: left; - } - - h3 { - width: 158px; - float: left; - margin: 0; - margin-left: 20px; - font-size: 18px; - line-height: 34px; - font-weight: normal; - } - } - } - .nav > li > a { color: #888; font-size: 14px; - line-height: 19px; padding: 0; background-color: #f5f5f5; - margin: 9px 0; + margin: ($header-height - 28) / 2 0; margin-left: 10px; border-radius: 40px; - height: 26px; - width: 26px; - line-height: 26px; + height: 28px; + width: 28px; + line-height: 28px; text-align: center; &:hover, &:focus, &:active { @@ -80,93 +44,84 @@ header { } } - /** NAV block with links and profile **/ - .nav { - float: right; - margin-right: 0; - } - .navbar-toggle { color: #666; margin: 0; border-radius: 0; + position: absolute; + right: 2px; &:hover { background-color: #EEE; } } } - - .turbolink-spinner { - font-size: 20px; - margin-right: 10px; - } - - @media (max-width: $screen-xs-max) { - border-width: 0; - font-size: 18px; - - .title { - @include str-truncated(70%); - } - - .navbar-collapse { - margin-top: 47px; - } - - .navbar-nav { - margin: 5px 0; - - .visible-xs, .visable-sm { - display: table-cell !important; - } - } - - li { - display: table-cell; - width: 1%; - - a { - text-align: center; - font-size: 18px !important; - } - } - } } - /** - * - * Logo holder - * - */ - .app_logo { + .header-logo { + border-bottom: 1px solid transparent; float: left; - margin-right: 9px; + height: $header-height; + width: $sidebar_width; a { float: left; - height: 46px; + height: $header-height; width: 100%; + padding: ($header-height - 36 ) / 2 8px; + + h3 { + width: 158px; + float: left; + margin: 0; + margin-left: 14px; + font-size: 18px; + line-height: $header-height - 14; + font-weight: normal; + } img { width: 36px; height: 36px; + float: left; } } + &:hover { background-color: #EEE; } } - /** - * - * Search box - * - */ + .header-content { + border-bottom: 1px solid #EEE; + padding-right: 35px; + height: $header-height; + + .title { + position: relative; + float: left; + margin: 0; + margin-left: 35px; + font-size: 18px; + line-height: $header-height; + font-weight: bold; + color: #444; + + @include str-truncated(37%); + + a { + color: #444; + &:hover { + text-decoration: underline; + } + } + } + } + .search { margin-right: 10px; margin-left: 10px; - margin-top: 8px; + margin-top: ($header-height - 28) / 2; form { margin: 0; @@ -174,6 +129,7 @@ header { } .search-input { + width: 220px; background-image: image-url("icon-search.png"); background-repeat: no-repeat; background-position: 10px; @@ -183,45 +139,80 @@ header { font-size: 13px; background-color: #f5f5f5; border-color: #f5f5f5; + + &:focus { + @include box-shadow(none); + outline: none; + border-color: #DDD; + background-color: #FFF; + } } } } -.search .search-input { - width: 300px; -} +@mixin collapsed-header { + .header-logo { + width: $sidebar_collapsed_width; -@media (max-width: 1200px) { - .search .search-input { - width: 200px; + h3 { + display: none; + } } -} -@media (max-width: $screen-xs-max) { - #nprogress .spinner { - right: 35px !important; + .header-content { + .title { + margin-left: 30px; + } } } @media (max-width: $screen-md-max) { - .header-collapsed, .header-expanded { - width: 52px; + header .container .title { + max-width: 43%; + } - h3 { - display: none; - } + .header-collapsed, .header-expanded { + @include collapsed-header; } } @media(min-width: $screen-md-max) { .header-collapsed { - width: 52px; - - h3 { - display: none; - } + @include collapsed-header; } .header-expanded { } } + +@media (max-width: $screen-xs-max) { + header .container { + font-size: 18px; + + .title { + max-width: 70%; + } + + .navbar-nav { + margin: 0px; + float: none !important; + + .visible-xs, .visable-sm { + display: table-cell !important; + } + } + + .navbar-collapse { + padding-left: 5px; + + li { + display: table-cell; + width: 1%; + + a { + margin-left: 8px !important; + } + } + } + } +} diff --git a/app/assets/stylesheets/generic/lists.scss b/app/assets/stylesheets/generic/lists.scss index 08bf6e943d2..c502d953c75 100644 --- a/app/assets/stylesheets/generic/lists.scss +++ b/app/assets/stylesheets/generic/lists.scss @@ -39,7 +39,6 @@ &:hover { background: $hover; - border-bottom: 1px solid darken($hover, 10%); } &:last-child { diff --git a/app/assets/stylesheets/generic/markdown_area.scss b/app/assets/stylesheets/generic/markdown_area.scss index eb39b6bb7e9..f94677d1925 100644 --- a/app/assets/stylesheets/generic/markdown_area.scss +++ b/app/assets/stylesheets/generic/markdown_area.scss @@ -52,6 +52,22 @@ transition: opacity 200ms ease-in-out; } +.md-area { + position: relative; +} + +.md-header ul { + float: left; +} + +.referenced-users { + padding: 10px 0; + color: #999; + margin-left: 10px; + margin-top: 1px; + margin-right: 130px; +} + .md-preview-holder { background: #FFF; border: 1px solid #ddd; diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/generic/mobile.scss index b7f6fac5223..f04c8eef904 100644 --- a/app/assets/stylesheets/generic/mobile.scss +++ b/app/assets/stylesheets/generic/mobile.scss @@ -19,6 +19,10 @@ } } + .referenced-users { + margin-right: 0; + } + .issues-filters, .dash-projects-filters, .check-all-holder { @@ -57,9 +61,24 @@ } .container .title { - margin-left: 6px !important; + margin-left: 15px !important; max-width: 70% !important; } + + .issue-info, .merge-request-info { + display: none; + } + + .issue-details { + .creator, + .page-title .btn-close { + display: none; + } + } + + %ul.notes .note-role, .note-actions { + display: none; + } } @media (max-width: $screen-sm-max) { diff --git a/app/assets/stylesheets/generic/sidebar.scss b/app/assets/stylesheets/generic/sidebar.scss index a80b5850803..65e06e14c73 100644 --- a/app/assets/stylesheets/generic/sidebar.scss +++ b/app/assets/stylesheets/generic/sidebar.scss @@ -1,6 +1,4 @@ .page-with-sidebar { - background: $background-color; - .sidebar-wrapper { position: fixed; top: 0; @@ -88,7 +86,7 @@ .nav-sidebar { margin-top: 29px; position: fixed; - top: 45px; + top: $header-height; width: $sidebar_width; } } @@ -102,13 +100,13 @@ padding-left: 50px; .sidebar-wrapper { - width: 52px; + width: $sidebar_collapsed_width; .nav-sidebar { margin-top: 29px; position: fixed; - top: 45px; - width: 52px; + top: $header-height; + width: $sidebar_collapsed_width; li a { padding-left: 18px; @@ -125,28 +123,20 @@ .collapse-nav a { left: 0px; - width: 52px; + width: $sidebar_collapsed_width; } .sidebar-user { .username { display: none; } - - .avatar { - margin-bottom: 10px; - } - - .logout-holder { - text-align: center; - } } } } .collapse-nav a { position: fixed; - top: 46px; + top: $header-height; left: 198px; font-size: 13px; background: transparent; @@ -190,9 +180,8 @@ bottom: 0; width: 100%; padding: 10px; - color: #fff; - .avatar { + .username { margin-top: 5px; } } diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss index e5590897947..66767cb13cb 100644 --- a/app/assets/stylesheets/generic/typography.scss +++ b/app/assets/stylesheets/generic/typography.scss @@ -23,6 +23,13 @@ pre { font-family: $monospace_font; } +code { + &.key-fingerprint { + background: $body-bg; + color: $text-color; + } +} + /** * Wiki typography * diff --git a/app/assets/stylesheets/generic/zen.scss b/app/assets/stylesheets/generic/zen.scss index 26afc21a6ab..7ab01187a02 100644 --- a/app/assets/stylesheets/generic/zen.scss +++ b/app/assets/stylesheets/generic/zen.scss @@ -1,15 +1,14 @@ .zennable { - position: relative; - - input { + .zen-toggle-comment { display: none; } .zen-enter-link { color: #888; position: absolute; - top: -26px; + top: 0px; right: 4px; + line-height: 40px; } .zen-leave-link { @@ -26,10 +25,12 @@ } } + // Hide the Enter link when we're in Zen mode input:checked ~ .zen-backdrop .zen-enter-link { display: none; } + // Show the Leave link when we're in Zen mode input:checked ~ .zen-backdrop .zen-leave-link { display: block; position: absolute; @@ -62,6 +63,9 @@ } } + // Make the placeholder text in the standard textarea the same color as the + // background, effectively hiding it + .zen-backdrop textarea::-webkit-input-placeholder { color: white; } @@ -78,6 +82,9 @@ color: white; } + // Make the color of the placeholder text in the Zenned-out textarea darker, + // so it becomes visible + input:checked ~ .zen-backdrop textarea::-webkit-input-placeholder { color: #999; } diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss index af9c83e5dc8..09e8d57a100 100644 --- a/app/assets/stylesheets/pages/dashboard.scss +++ b/app/assets/stylesheets/pages/dashboard.scss @@ -29,7 +29,7 @@ line-height: 24px; .str-truncated { - max-width: 72%; + max-width: 76%; } a { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 3572f33e91f..ed938f86b35 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -145,3 +145,9 @@ h2.issue-title { .issue-form .select2-container { width: 250px !important; } + +.issues-holder { + .issue-info { + margin-left: 20px; + } +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 3165396a94d..f5ac7bd8805 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -123,38 +123,31 @@ .mr-state-widget { font-size: 13px; - background: #F9F9F9; + background: #FAFAFA; margin-bottom: 20px; color: #666; - border: 1px solid #EEE; - @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09)); + border: 1px solid #e5e5e5; + @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.05)); + @include border-radius(3px); .ci_widget { padding: 10px 15px; font-size: 15px; - border-bottom: 1px solid #BBB; - color: #777; - background-color: $background-color; + border-bottom: 1px solid #EEE; &.ci-success { color: $gl-success; - border-color: $gl-success; - background-color: #F1FAF1; } &.ci-pending, &.ci-running { color: $gl-warning; - border-color: $gl-warning; - background-color: #FAF5F1; } &.ci-failed, &.ci-canceled, &.ci-error { color: $gl-danger; - border-color: $gl-danger; - background-color: #FAF1F1; } } @@ -162,7 +155,8 @@ padding: 10px 15px; h4 { - font-weight: normal; + font-weight: bold; + margin: 5px 0; } p:last-child { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index a0522030785..203f9374cee 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -39,11 +39,8 @@ .new_note, .edit_note { .buttons { - float: left; margin-top: 8px; - } - .clearfix { - margin-bottom: 0; + margin-bottom: 3px; } .note-preview-holder { @@ -82,7 +79,6 @@ .note-form-actions { background: #F9F9F9; - height: 45px; .note-form-option { margin-top: 8px; diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 5b528b38d36..5a5fbc468a3 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -84,8 +84,9 @@ } .btn { - line-height: 36px; - height: 56px; + line-height: 40px; + height: 42px; + padding: 0px 12px; img { width: 32px; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index ee8c746d2df..e19b2eafa43 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -15,18 +15,16 @@ } .project-home-panel { - margin-bottom: 20px; + margin-top: 10px; + margin-bottom: 15px; position: relative; padding-left: 65px; - border-bottom: 1px solid #DDD; - padding-bottom: 10px; - padding-top: 5px; min-height: 50px; .project-identicon-holder { position: absolute; left: 0; - top: -10px; + top: -14px; .avatar { width: 50px; @@ -48,14 +46,16 @@ } .project-home-desc { + color: $gray; + float: left; font-size: 16px; line-height: 1.3; margin-right: 250px; - } - .project-home-desc { - float: left; - color: $gray; + // Render Markdown-generated HTML inline for this block + p { + display: inline; + } } } @@ -210,8 +210,13 @@ ul.nav.nav-projects-tabs { } .panel { + @include border-radius(3px); + .panel-heading, .panel-footer { - background-color: #fcfcfc; + font-weight: normal; + background-color: transparent; + color: #666; + border-color: #EEE; } .actions { @@ -225,7 +230,7 @@ ul.nav.nav-projects-tabs { } .nav { - margin-bottom: 10px; + margin-bottom: 15px; } } diff --git a/app/assets/stylesheets/themes/gitlab-theme.scss b/app/assets/stylesheets/themes/gitlab-theme.scss index 9b8e3d8e291..10fcaf18fa9 100644 --- a/app/assets/stylesheets/themes/gitlab-theme.scss +++ b/app/assets/stylesheets/themes/gitlab-theme.scss @@ -1,8 +1,9 @@ @mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) { header { &.navbar-gitlab { - .app_logo { + .header-logo { background-color: $color-darker; + border-color: $color-darker; a { color: $color-light; @@ -19,8 +20,6 @@ } .page-with-sidebar { - background: $color-darker; - .collapse-nav a { color: #FFF; background: $color; @@ -31,8 +30,12 @@ border-right: 1px solid $color-darker; .sidebar-user { - a { - color: $color-light; + color: $color-light; + + &:hover { + background-color: $color-dark; + color: #FFF; + text-decoration: none; } } } diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 4c35622fff1..a01e2a907d7 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -38,11 +38,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :twitter_sharing_enabled, :sign_in_text, :home_page_url, + :after_sign_out_path, :max_attachment_size, :default_project_visibility, :default_snippet_visibility, :restricted_signup_domains_raw, :version_check_enabled, + :user_oauth_applications, restricted_visibility_levels: [], ) end diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb index c301e61d1c7..285e8495342 100644 --- a/app/controllers/admin/deploy_keys_controller.rb +++ b/app/controllers/admin/deploy_keys_controller.rb @@ -1,13 +1,8 @@ class Admin::DeployKeysController < Admin::ApplicationController before_action :deploy_keys, only: [:index] - before_action :deploy_key, only: [:show, :destroy] + before_action :deploy_key, only: [:destroy] def index - - end - - def show - end def new diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 2dfae13ac5c..4d3e48f7f81 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -47,7 +47,7 @@ class Admin::GroupsController < Admin::ApplicationController end def destroy - @group.destroy + DestroyGroupService.new(@group, current_user).execute redirect_to admin_groups_path, notice: 'Group was successfully deleted.' end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e5da94b2327..62d46a5482e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -89,7 +89,7 @@ class ApplicationController < ActionController::Base end def after_sign_out_path_for(resource) - new_user_session_path + current_application_settings.after_sign_out_path || new_user_session_path end def abilities diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index a11c554a2af..040255f08e6 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -66,7 +66,11 @@ class Groups::GroupMembersController < Groups::ApplicationController @group_member.destroy redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.") else - return render_403 + if @group.last_owner?(current_user) + redirect_to(dashboard_groups_path, alert: "You can not leave #{group.name} group because you're the last owner. Transfer or delete the group.") + else + return render_403 + end end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 34f0b257db3..2e381822e42 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -82,7 +82,7 @@ class GroupsController < Groups::ApplicationController end def destroy - @group.destroy + DestroyGroupService.new(@group, current_user).execute redirect_to root_path, notice: 'Group was removed.' end diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index 507b8290a2b..fc31118124b 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -1,6 +1,8 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController + include Gitlab::CurrentSettings include PageLayoutHelper + before_action :verify_user_oauth_applications_enabled before_action :authenticate_user! layout 'profile' @@ -32,6 +34,12 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController private + def verify_user_oauth_applications_enabled + return if current_application_settings.user_oauth_applications? + + redirect_to applications_profile_url + end + def set_application @application = current_user.oauth_applications.find(params[:id]) end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index dcd949a71de..765adaf2128 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -1,4 +1,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController + + protect_from_forgery except: [:kerberos, :saml] + Gitlab.config.omniauth.providers.each do |provider| define_method provider['name'] do handle_omniauth @@ -21,7 +24,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController @user = Gitlab::LDAP::User.new(oauth) @user.save if @user.changed? # will also save new users gl_user = @user.gl_user - gl_user.remember_me = true if @user.persisted? + gl_user.remember_me = params[:remember_me] if @user.persisted? # Do additional LDAP checks for the user filter and EE features if @user.allowed? diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index b762518d377..100d3d3b317 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -13,27 +13,20 @@ class Projects::BlobController < Projects::ApplicationController before_action :commit, except: [:new, :create] before_action :blob, except: [:new, :create] before_action :from_merge_request, only: [:edit, :update] - before_action :after_edit_path, only: [:edit, :update] before_action :require_branch_head, only: [:edit, :update] + before_action :editor_variables, except: [:show, :preview, :diff] + before_action :after_edit_path, only: [:edit, :update] def new commit unless @repository.empty? end def create - file_path = File.join(@path, File.basename(params[:file_name])) - result = Files::CreateService.new( - @project, - current_user, - params.merge(new_branch: sanitized_new_branch_name), - @ref, - file_path - ).execute + result = Files::CreateService.new(@project, current_user, @commit_params).execute if result[:status] == :success flash[:notice] = "Your changes have been successfully committed" - ref = sanitized_new_branch_name.presence || @ref - redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(ref, file_path)) + redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) else flash[:alert] = result[:message] render :new @@ -48,22 +41,10 @@ class Projects::BlobController < Projects::ApplicationController end def update - result = Files::UpdateService. - new( - @project, - current_user, - params.merge(new_branch: sanitized_new_branch_name), - @ref, - @path - ).execute + result = Files::UpdateService.new(@project, current_user, @commit_params).execute if result[:status] == :success flash[:notice] = "Your changes have been successfully committed" - - if from_merge_request - from_merge_request.reload_code - end - redirect_to after_edit_path else flash[:alert] = result[:message] @@ -80,12 +61,11 @@ class Projects::BlobController < Projects::ApplicationController end def destroy - result = Files::DeleteService.new(@project, current_user, params, @ref, @path).execute + result = Files::DeleteService.new(@project, current_user, @commit_params).execute if result[:status] == :success flash[:notice] = "Your changes have been successfully committed" - redirect_to namespace_project_tree_path(@project.namespace, @project, - @ref) + redirect_to namespace_project_tree_path(@project.namespace, @project, @target_branch) else flash[:alert] = result[:message] render :show @@ -135,7 +115,6 @@ class Projects::BlobController < Projects::ApplicationController @id = params[:id] @ref, @path = extract_ref(@id) - rescue InvalidPathError not_found! end @@ -145,8 +124,8 @@ class Projects::BlobController < Projects::ApplicationController if from_merge_request diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + "#file-path-#{hexdigest(@path)}" - elsif sanitized_new_branch_name.present? - namespace_project_blob_path(@project.namespace, @project, File.join(sanitized_new_branch_name, @path)) + elsif @target_branch.present? + namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) else namespace_project_blob_path(@project.namespace, @project, @id) end @@ -160,4 +139,25 @@ class Projects::BlobController < Projects::ApplicationController def sanitized_new_branch_name @new_branch ||= sanitize(strip_tags(params[:new_branch])) end + + def editor_variables + @current_branch = @ref + @target_branch = (sanitized_new_branch_name || @ref) + + @file_path = + if action_name.to_s == 'create' + File.join(@path, File.basename(params[:file_name])) + else + @path + end + + @commit_params = { + file_path: @file_path, + current_branch: @current_branch, + target_branch: @target_branch, + commit_message: params[:commit_message], + file_content: params[:content], + file_content_encoding: params[:encoding] + } + end end diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 8c1bbf76917..40e2b37912b 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -18,10 +18,6 @@ class Projects::DeployKeysController < Projects::ApplicationController @available_public_keys -= @available_project_keys end - def show - @key = @project.deploy_keys.find(params[:id]) - end - def new @key = @project.deploy_keys.new diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index c7467e9b2f5..71d3051ab88 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -2,10 +2,13 @@ require 'gitlab/satellite/satellite' class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled - before_action :merge_request, only: [:edit, :update, :show, :diffs, :automerge, :automerge_check, :ci_status, :toggle_subscription] - before_action :closes_issues, only: [:edit, :update, :show, :diffs] - before_action :validates_merge_request, only: [:show, :diffs] - before_action :define_show_vars, only: [:show, :diffs] + before_action :merge_request, only: [ + :edit, :update, :show, :diffs, :commits, :automerge, :automerge_check, + :ci_status, :toggle_subscription + ] + 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] # Allow read any merge_request before_action :authorize_read_merge_request! @@ -27,7 +30,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_requests = @merge_requests.full_search(terms) end end - + @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE) respond_to do |format| @@ -67,6 +70,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def commits + render 'show' + end + def new params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index d7fbc979067..b82b6f45d59 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -2,8 +2,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController # Authorize before_action :authorize_admin_project!, except: :leave - layout "project_settings" - def index @project_members = @project.project_members @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) @@ -73,10 +71,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def leave + if @project.namespace == current_user.namespace + return redirect_to(:back, alert: 'You can not leave your own project. Transfer or delete the project.') + end + @project.project_members.find_by(user_id: current_user).destroy respond_to do |format| - format.html { redirect_to :back } + format.html { redirect_to dashboard_path } format.js { render nothing: true } end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index dc430351551..be5968cd7b0 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -97,18 +97,15 @@ class ProjectsController < ApplicationController return access_denied! unless can?(current_user, :remove_project, @project) ::Projects::DestroyService.new(@project, current_user, {}).execute + flash[:alert] = 'Project deleted.' - respond_to do |format| - format.html do - flash[:alert] = 'Project deleted.' - - if request.referer.include?('/admin') - redirect_to admin_namespaces_projects_path - else - redirect_to dashboard_path - end - end + if request.referer.include?('/admin') + redirect_to admin_namespaces_projects_path + else + redirect_to dashboard_path end + rescue Projects::DestroyService::DestroyError => ex + redirect_to edit_project_path(@project), alert: ex.message end def autocomplete_sources @@ -154,7 +151,17 @@ class ProjectsController < ApplicationController end def markdown_preview - render text: view_context.markdown(params[:md_text]) + text = params[:text] + + ext = Gitlab::ReferenceExtractor.new(@project, current_user) + ext.analyze(text) + + render json: { + body: view_context.markdown(text), + references: { + users: ext.users.map(&:username) + } + } end private diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index b89b4c27350..4d976fe6630 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -2,6 +2,7 @@ class SessionsController < Devise::SessionsController include AuthenticatesWithTwoFactor prepend_before_action :authenticate_with_two_factor, only: [:create] + before_action :auto_sign_in_with_provider, only: [:new] def new redirect_path = @@ -75,6 +76,21 @@ class SessionsController < Devise::SessionsController end end + def auto_sign_in_with_provider + provider = Gitlab.config.omniauth.auto_sign_in_with_provider + return unless provider.present? + + # Auto sign in with an Omniauth provider only if the standard "you need to sign-in" alert is + # registered or no alert at all. In case of another alert (such as a blocked user), it is safer + # to do nothing to prevent redirection loops with certain Omniauth providers. + return unless flash[:alert].blank? || flash[:alert] == I18n.t('devise.failure.unauthenticated') + + # Prevent alert from popping up on the first page shown after authentication. + flash[:alert] = nil + + redirect_to omniauth_authorize_path(:user, provider.to_sym) + end + def valid_otp_attempt?(user) user.valid_otp?(user_params[:otp_attempt]) || user.invalidate_otp_backup_code!(user_params[:otp_attempt]) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 89dcdf57798..a539ec49f7a 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -279,10 +279,6 @@ module ApplicationHelper html_options end - def escaped_autolink(text) - auto_link ERB::Util.html_escape(text), link: :urls - end - def promo_host 'about.gitlab.com' end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 241d6075c9f..63c3ff5674d 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -19,6 +19,10 @@ module ApplicationSettingsHelper current_application_settings.sign_in_text end + def user_oauth_applications? + current_application_settings.user_oauth_applications + end + # Return a group of checkboxes that use Bootstrap's button plugin for a # toggle button effect. def restricted_level_checkboxes(help_block_id) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index d89f7b4a28d..2777944fc9d 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -1,3 +1,5 @@ +require 'nokogiri' + module GitlabMarkdownHelper include Gitlab::Markdown @@ -21,36 +23,44 @@ module GitlabMarkdownHelper gfm_body = gfm(escaped_body, {}, html_options) - gfm_body.gsub!(%r{<a.*?>.*?</a>}m) do |match| - "</a>#{match}#{link_to("", url, html_options)[0..-5]}" # "</a>".length +1 + fragment = Nokogiri::XML::DocumentFragment.parse(gfm_body) + if fragment.children.size == 1 && fragment.children[0].name == 'a' + # Fragment has only one node, and it's a link generated by `gfm`. + # Replace it with our requested link. + text = fragment.children[0].text + fragment.children[0].replace(link_to(text, url, html_options)) + else + # Traverse the fragment's first generation of children looking for pure + # text, wrapping anything found in the requested link + fragment.children.each do |node| + next unless node.text? + node.replace(link_to(node.text, url, html_options)) + end end - link_to(gfm_body.html_safe, url, html_options) + fragment.to_html.html_safe end + MARKDOWN_OPTIONS = { + no_intra_emphasis: true, + tables: true, + fenced_code_blocks: true, + strikethrough: true, + lax_spacing: true, + space_after_headers: true, + superscript: true, + footnotes: true + }.freeze + def markdown(text, options={}) unless @markdown && options == @options @options = options - options.merge!( - # Handled further down the line by Gitlab::Markdown::SanitizationFilter - escape_html: false - ) - # see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch rend = Redcarpet::Render::GitlabHTML.new(self, user_color_scheme_class, options) # see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use - @markdown = Redcarpet::Markdown.new(rend, - no_intra_emphasis: true, - tables: true, - fenced_code_blocks: true, - strikethrough: true, - lax_spacing: true, - space_after_headers: true, - superscript: true, - footnotes: true - ) + @markdown = Redcarpet::Markdown.new(rend, MARKDOWN_OPTIONS) end @markdown.render(text).html_safe diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index f8df39d236a..94ce6646634 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -294,4 +294,16 @@ module ProjectsHelper nil end end + + def user_max_access_in_project(user, project) + level = project.team.max_member_access(user) + + if level + Gitlab::Access.options_with_owner.key(level) + end + end + + def leave_project_message(project) + "Are you sure you want to leave \"#{project.name}\" project?" + end end diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index a1d263d9d3a..77727337f07 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -89,7 +89,7 @@ module TabHelper def project_tab_class return "active" if current_page?(controller: "/projects", action: :edit, id: @project) - if ['services', 'hooks', 'deploy_keys', 'project_members', 'protected_branches'].include? controller.controller_name + if ['services', 'hooks', 'deploy_keys', 'protected_branches'].include? controller.controller_name "active" end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index d5123249c53..80463ee8841 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -18,6 +18,8 @@ # default_project_visibility :integer # default_snippet_visibility :integer # restricted_signup_domains :text +# user_oauth_applications :bool default(TRUE) +# after_sign_out_path :string(255) # class ApplicationSetting < ActiveRecord::Base @@ -30,6 +32,10 @@ class ApplicationSetting < ActiveRecord::Base format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, if: :home_page_url_column_exist + validates :after_sign_out_path, + allow_blank: true, + format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" } + validates_each :restricted_visibility_levels do |record, attr, value| unless value.nil? value.each do |level| diff --git a/app/models/commit.rb b/app/models/commit.rb index f02fe240540..9d721661629 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -172,10 +172,8 @@ class Commit @raw.send(m, *args, &block) end - def respond_to?(method) - return true if @raw.respond_to?(method) - - super + def respond_to_missing?(method, include_private = false) + @raw.respond_to?(method, include_private) || super end # Truncate sha to 8 characters diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 6f9f54d08cc..10c39cb1ece 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -67,7 +67,13 @@ module Mentionable # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+. def create_cross_references!(p = project, a = author, without = []) - refs = references(p) - without + refs = references(p) + + # We're using this method instead of Array diffing because that requires + # both of the object's `hash` values to be the same, which may not be the + # case for otherwise identical Commit objects. + refs.reject! { |ref| without.include?(ref) } + refs.each do |ref| Note.create_cross_reference_note(ref, local_reference, a) end diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb index 33b4814d7ec..660e58b876d 100644 --- a/app/models/concerns/taskable.rb +++ b/app/models/concerns/taskable.rb @@ -1,4 +1,5 @@ require 'task_list' +require 'task_list/filter' # Contains functionality for objects that can have task lists in their # descriptions. Task list items can be added with Markdown like "* [x] Fix diff --git a/app/models/group.rb b/app/models/group.rb index b4e908c5602..051c672cb33 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -101,10 +101,14 @@ class Group < Namespace end def post_create_hook + Gitlab::AppLogger.info("Group \"#{name}\" was created") + system_hook_service.execute_hooks_for(self, :create) end def post_destroy_hook + Gitlab::AppLogger.info("Group \"#{name}\" was removed") + system_hook_service.execute_hooks_for(self, :destroy) end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 5690c375b96..f1f9f23b12c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -197,7 +197,6 @@ class MergeRequest < ActiveRecord::Base def update_merge_request_diff if source_branch_changed? || target_branch_changed? reload_code - mark_as_unchecked end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 211dfa76b81..03d2ab165ea 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -72,7 +72,7 @@ class Namespace < ActiveRecord::Base path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") # Users with the great usernames of "." or ".." would end up with a blank username. - # Work around that by setting their username to "blank", followed by a counter. + # Work around that by setting their username to "blank", followed by a counter. path = "blank" if path.blank? counter = 0 @@ -99,7 +99,18 @@ class Namespace < ActiveRecord::Base end def rm_dir - gitlab_shell.rm_namespace(path) + # Move namespace directory into trash. + # We will remove it later async + new_path = "#{path}+#{id}+deleted" + + if gitlab_shell.mv_namespace(path, new_path) + message = "Namespace directory \"#{path}\" moved to \"#{new_path}\"" + Gitlab::AppLogger.info message + + # Remove namespace directroy async with delay so + # GitLab has time to remove all projects first + GitlabShellWorker.perform_in(5.minutes, :rm_namespace, new_path) + end end def move_dir diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index 949a4d7111b..a9354754686 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -40,6 +40,12 @@ class GitlabCiService < CiService def execute(data) return unless supported_events.include?(data[:object_kind]) + ci_yaml_file = ci_yaml_file(data) + + if ci_yaml_file + data.merge!(ci_yaml_file: ci_yaml_file) + end + service_hook.execute(data) end @@ -123,6 +129,14 @@ class GitlabCiService < CiService private + def ci_yaml_file(data) + ref = data[:checkout_sha] + repo = project.repository + commit = repo.commit(ref) + blob = Gitlab::Git::Blob.find(repo, commit.id, ".gitlab-ci.yml") + blob && blob.data + end + def fork_registration_path project_url.sub(/projects\/\d*/, "#{API_PREFIX}/forks") end diff --git a/app/models/repository.rb b/app/models/repository.rb index 1b8c74028d9..2c6347222aa 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -163,10 +163,8 @@ class Repository end end - def respond_to?(method) - return true if raw_repository.respond_to?(method) - - super + def respond_to_missing?(method, include_private = false) + raw_repository.respond_to?(method, include_private) || super end def blob_at(sha, path) @@ -370,8 +368,55 @@ class Repository @root_ref ||= raw_repository.root_ref end + def commit_file(user, path, content, message, ref) + path[0] = '' if path[0] == '/' + + committer = user_to_comitter(user) + options = {} + options[:committer] = committer + options[:author] = committer + options[:commit] = { + message: message, + branch: ref + } + + options[:file] = { + content: content, + path: path + } + + Gitlab::Git::Blob.commit(raw_repository, options) + end + + def remove_file(user, path, message, ref) + path[0] = '' if path[0] == '/' + + committer = user_to_comitter(user) + options = {} + options[:committer] = committer + options[:author] = committer + options[:commit] = { + message: message, + branch: ref + } + + options[:file] = { + path: path + } + + Gitlab::Git::Blob.remove(raw_repository, options) + end + private + def user_to_comitter(user) + { + email: user.email, + name: user.name, + time: Time.now + } + end + def cache @cache ||= RepositoryCache.new(path_with_namespace) end diff --git a/app/models/user.rb b/app/models/user.rb index c1bb51e86fc..596dc7ea33a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -483,7 +483,7 @@ class User < ActiveRecord::Base end def sanitize_attrs - %w(name username skype linkedin twitter bio).each do |attr| + %w(name username skype linkedin twitter).each do |attr| value = self.send(attr) self.send("#{attr}=", Sanitize.clean(value)) if value.present? end @@ -655,6 +655,12 @@ class User < ActiveRecord::Base end end + def namespaces + namespace_ids = groups.pluck(:id) + namespace_ids.push(namespace.id) + Namespace.where(id: namespace_ids) + end + def oauth_authorized_tokens Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil) end diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb index d259b4efca6..9017a63af3b 100644 --- a/app/services/delete_user_service.rb +++ b/app/services/delete_user_service.rb @@ -4,6 +4,12 @@ class DeleteUserService user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user' user else + user.personal_projects.each do |project| + # Skip repository removal because we remove directory with namespace + # that contain all this repositories + ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute + end + user.destroy end end diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb new file mode 100644 index 00000000000..d929a676293 --- /dev/null +++ b/app/services/destroy_group_service.rb @@ -0,0 +1,17 @@ +class DestroyGroupService + attr_accessor :group, :current_user + + def initialize(group, user) + @group, @current_user = group, user + end + + def execute + @group.projects.each do |project| + # Skip repository removal because we remove directory with namespace + # that contain all this repositories + ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute + end + + @group.destroy + end +end diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index bd245100955..f587ee266da 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -1,11 +1,34 @@ module Files class BaseService < ::BaseService - attr_reader :ref, :path + class ValidationError < StandardError; end - def initialize(project, user, params, ref, path = nil) - @project, @current_user, @params = project, user, params.dup - @ref = ref - @path = path + def execute + @current_branch = params[:current_branch] + @target_branch = params[:target_branch] + @commit_message = params[:commit_message] + @file_path = params[:file_path] + @file_content = if params[:file_content_encoding] == 'base64' + Base64.decode64(params[:file_content]) + else + params[:file_content] + end + + # Validate parameters + validate + + # Create new branch if it different from current_branch + if @target_branch != @current_branch + create_target_branch + end + + if sha = commit + after_commit(sha, @target_branch) + success + else + error("Something went wrong. Your changes were not committed") + end + rescue ValidationError => ex + error(ex.message) end private @@ -13,5 +36,52 @@ module Files def repository project.repository end + + def after_commit(sha, branch) + commit = repository.commit(sha) + full_ref = 'refs/heads/' + branch + old_sha = commit.parent_id || Gitlab::Git::BLANK_SHA + GitPushService.new.execute(project, current_user, old_sha, sha, full_ref) + end + + def current_branch + @current_branch ||= params[:current_branch] + end + + def target_branch + @target_branch ||= params[:target_branch] + end + + def raise_error(message) + raise ValidationError.new(message) + end + + def validate + allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch) + + unless allowed + raise_error("You are not allowed to push into this branch") + end + + unless project.empty_repo? + unless repository.branch_names.include?(@current_branch) + raise_error("You can only create files if you are on top of a branch") + end + + if @current_branch != @target_branch + if repository.branch_names.include?(@target_branch) + raise_error("Branch with such name already exists. You need to switch to this branch in order to make changes") + end + end + end + end + + def create_target_branch + result = CreateBranchService.new(project, current_user).execute(@target_branch, @current_branch) + + unless result[:status] == :success + raise_error("Something went wrong when we tried to create #{@target_branch} for you") + end + end end end diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index 23833aa78ec..91d715b2d63 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -1,52 +1,30 @@ require_relative "base_service" module Files - class CreateService < BaseService - def execute - allowed = Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) + class CreateService < Files::BaseService + def commit + repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch) + end - unless allowed - return error("You are not allowed to create file in this branch") - end + def validate + super - file_name = File.basename(path) - file_path = path + file_name = File.basename(@file_path) unless file_name =~ Gitlab::Regex.file_name_regex - return error( + raise_error( 'Your changes could not be committed, because the file name ' + Gitlab::Regex.file_name_regex_message ) end - if project.empty_repo? - # everything is ok because repo does not have a commits yet - else - unless repository.branch_names.include?(ref) - return error("You can only create files if you are on top of a branch") - end - - blob = repository.blob_at_branch(ref, file_path) + unless project.empty_repo? + blob = repository.blob_at_branch(@current_branch, @file_path) if blob - return error("Your changes could not be committed, because file with such name exists") + raise_error("Your changes could not be committed, because file with such name exists") end end - - - new_file_action = Gitlab::Satellite::NewFileAction.new(current_user, project, ref, file_path) - created_successfully = new_file_action.commit!( - params[:content], - params[:commit_message], - params[:encoding], - params[:new_branch] - ) - - if created_successfully - success - else - error("Your changes could not be committed, because the file has been changed") - end end end end diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb index 1497a0f883b..27c881c3430 100644 --- a/app/services/files/delete_service.rb +++ b/app/services/files/delete_service.rb @@ -1,36 +1,9 @@ require_relative "base_service" module Files - class DeleteService < BaseService - def execute - allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) - - unless allowed - return error("You are not allowed to push into this branch") - end - - unless repository.branch_names.include?(ref) - return error("You can only create files if you are on top of a branch") - end - - blob = repository.blob_at_branch(ref, path) - - unless blob - return error("You can only edit text files") - end - - delete_file_action = Gitlab::Satellite::DeleteFileAction.new(current_user, project, ref, path) - - deleted_successfully = delete_file_action.commit!( - nil, - params[:commit_message] - ) - - if deleted_successfully - success - else - error("Your changes could not be committed, because the file has been changed") - end + class DeleteService < Files::BaseService + def commit + repository.remove_file(current_user, @file_path, @commit_message, @target_branch) end end end diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index 0724d3ae634..a20903c6f02 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -1,39 +1,9 @@ require_relative "base_service" module Files - class UpdateService < BaseService - def execute - allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) - - unless allowed - return error("You are not allowed to push into this branch") - end - - unless repository.branch_names.include?(ref) - return error("You can only create files if you are on top of a branch") - end - - blob = repository.blob_at_branch(ref, path) - - unless blob - return error("You can only edit text files") - end - - edit_file_action = Gitlab::Satellite::EditFileAction.new(current_user, project, ref, path) - edit_file_action.commit!( - params[:content], - params[:commit_message], - params[:encoding], - params[:new_branch] - ) - - success - rescue Gitlab::Satellite::CheckoutFailed => ex - error("Your changes could not be committed because ref '#{ref}' could not be checked out", 400) - rescue Gitlab::Satellite::CommitFailed => ex - error("Your changes could not be committed. Maybe there was nothing to commit?", 409) - rescue Gitlab::Satellite::PushFailed => ex - error("Your changes could not be committed. Maybe the file was changed by another process?", 409) + class UpdateService < Files::BaseService + def commit + repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch) end end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index bdf36af02fd..cde65349d5c 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -127,7 +127,8 @@ class GitPushService end def is_default_branch?(ref) - Gitlab::Git.branch_ref?(ref) && Gitlab::Git.ref_name(ref) == project.default_branch + Gitlab::Git.branch_ref?(ref) && + (Gitlab::Git.ref_name(ref) == project.default_branch || project.default_branch.nil?) end def commit_user(commit) diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index c5769a5ad27..1d99223cfe6 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -20,4 +20,10 @@ class IssuableBaseService < BaseService SystemNoteService.change_title( issuable, issuable.project, current_user, old_title) end + + def create_branch_change_note(issuable, branch_type, old_branch, new_branch) + SystemNoteService.change_branch( + issuable, issuable.project, current_user, branch_type, + old_branch, new_branch) + end end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 34fd59d6927..4f6c6cba9a9 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -5,10 +5,11 @@ require_relative 'close_service' module MergeRequests class UpdateService < MergeRequests::BaseService def execute(merge_request) - # We dont allow change of source/target projects + # We don't allow change of source/target projects and source branch # after merge request was created params.except!(:source_project_id) params.except!(:target_project_id) + params.except!(:source_branch) state = params[:state_event] @@ -41,6 +42,12 @@ module MergeRequests ) end + if merge_request.previous_changes.include?('target_branch') + create_branch_change_note(merge_request, 'target', + merge_request.previous_changes['target_branch'].first, + merge_request.target_branch) + end + if merge_request.previous_changes.include?('milestone_id') create_milestone_note(merge_request) end @@ -54,6 +61,11 @@ module MergeRequests create_title_change_note(merge_request, merge_request.previous_changes['title'].first) end + if merge_request.previous_changes.include?('target_branch') || + merge_request.previous_changes.include?('source_branch') + merge_request.mark_as_unchecked + end + merge_request.notice_added_references(merge_request.project, current_user) execute_hooks(merge_request, 'update') end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 7e1d753b021..403f419ec50 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -1,28 +1,69 @@ module Projects class DestroyService < BaseService + include Gitlab::ShellAdapter + + class DestroyError < StandardError; end + + DELETED_FLAG = '+deleted' + def execute return false unless can?(current_user, :remove_project, project) project.team.truncate project.repository.expire_cache unless project.empty_repo? - if project.destroy - GitlabShellWorker.perform_async( - :remove_repository, - project.path_with_namespace - ) + repo_path = project.path_with_namespace + wiki_path = repo_path + '.wiki' - GitlabShellWorker.perform_async( - :remove_repository, - project.path_with_namespace + ".wiki" - ) + Project.transaction do + project.destroy! - project.satellite.destroy + unless remove_repository(repo_path) + raise_error('Failed to remove project repository. Please try again or contact administrator') + end - log_info("Project \"#{project.name}\" was removed") - system_hook_service.execute_hooks_for(project, :destroy) - true + unless remove_repository(wiki_path) + raise_error('Failed to remove wiki repository. Please try again or contact administrator') + end end + + project.satellite.destroy + log_info("Project \"#{project.name}\" was removed") + system_hook_service.execute_hooks_for(project, :destroy) + true + end + + private + + def remove_repository(path) + # Skip repository removal. We use this flag when remove user or group + return true if params[:skip_repo] == true + + # There is a possibility project does not have repository or wiki + return true unless gitlab_shell.exists?(path + '.git') + + new_path = removal_path(path) + + if gitlab_shell.mv_repository(path, new_path) + log_info("Repository \"#{path}\" moved to \"#{new_path}\"") + GitlabShellWorker.perform_in(5.minutes, :remove_repository, new_path) + else + false + end + end + + def raise_error(message) + raise DestroyError.new(message) + end + + # Build a path for removing repositories + # We use `+` because its not allowed by GitLab so user can not create + # project with name cookies+119+deleted and capture someone stalled repository + # + # gitlab/cookies.git -> gitlab/cookies+119+deleted.git + # + def removal_path(path) + "#{path}+#{project.id}#{DELETED_FLAG}" end end end diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb index b91590a1a90..0004a399f47 100644 --- a/app/services/projects/participants_service.rb +++ b/app/services/projects/participants_service.rb @@ -38,13 +38,13 @@ module Projects def groups current_user.authorized_groups.sort_by(&:path).map do |group| count = group.users.count - { username: group.path, name: "#{group.name} (#{count})" } + { username: group.path, name: group.name, count: count } end end def all_members count = project.team.members.flatten.count - [{ username: "all", name: "All Project and Group Members (#{count})" }] + [{ username: "all", name: "All Project and Group Members", count: count }] end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 1527ae0486d..b6801a92330 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -149,6 +149,25 @@ class SystemNoteService create_note(noteable: noteable, project: project, author: author, note: body) end + # Called when a branch in Noteable is changed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # branch_type - 'source' or 'target' + # old_branch - old branch name + # new_branch - new branch nmae + # + # Example Note text: + # + # "Target branch changed from `Old` to `New`" + # + # Returns the created Note object + def self.change_branch(noteable, project, author, branch_type, old_branch, new_branch) + body = "#{branch_type} branch changed from `#{old_branch}` to `#{new_branch}`".capitalize + create_note(noteable: noteable, project: project, author: author, note: body) + end + # Called when a Mentionable references a Noteable # # noteable - Noteable object being referenced diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 4ceae814805..188a08940ab 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -30,7 +30,7 @@ .checkbox = f.label :twitter_sharing_enabled do = f.check_box :twitter_sharing_enabled, :'aria-describedby' => 'twitter_help_block' - %strong Twitter enabled + Twitter enabled %span.help-block#twitter_help_block Show users a button to share their newly created public or internal projects on twitter .form-group .col-sm-offset-2.col-sm-10 @@ -70,6 +70,11 @@ = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block' %span.help-block#home_help_block We will redirect non-logged in users to this page .form-group + = f.label :after_sign_out_path, class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block' + %span.help-block#after_sign_out_path_help_block We will redirect users to this page after they sign out + .form-group = f.label :sign_in_text, class: 'control-label col-sm-2' .col-sm-10 = f.text_area :sign_in_text, class: 'form-control', rows: 4 @@ -83,6 +88,13 @@ .col-sm-10 = f.text_area :restricted_signup_domains_raw, placeholder: 'domain.com', class: 'form-control' .help-block Only users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com + .form_group + = f.label :user_oauth_applications, 'User OAuth applications', class: 'control-label col-sm-2' + .col-sm-10 + .checkbox + = f.label :user_oauth_applications do + = f.check_box :user_oauth_applications + Allow users to register any application to use GitLab as an OAuth provider .form-actions = f.submit 'Save', class: 'btn btn-primary' diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml index 267c9a52921..17dffebd360 100644 --- a/app/views/admin/broadcast_messages/index.html.haml +++ b/app/views/admin/broadcast_messages/index.html.haml @@ -22,11 +22,11 @@ .form-group.js-toggle-colors-container.hide = f.label :color, "Background Color", class: 'control-label' .col-sm-10 - = f.color_field :color, value: "#AA33EE", class: "form-control" + = f.color_field :color, value: "#eb9532", class: "form-control" .form-group.js-toggle-colors-container.hide = f.label :font, "Font Color", class: 'control-label' .col-sm-10 - = f.color_field :font, value: "#224466", class: "form-control" + = f.color_field :font, value: "#FFFFFF", class: "form-control" .form-group = f.label :starts_at, class: 'control-label' .col-sm-10.datetime-controls diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 367d25cd6a1..6405a69fad3 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -19,8 +19,7 @@ = link_to admin_deploy_key_path(deploy_key) do %strong= deploy_key.title %td - %span - (#{deploy_key.fingerprint}) + %code.key-fingerprint= deploy_key.fingerprint %td %span.cgray added #{time_ago_with_tooltip(deploy_key.created_at)} diff --git a/app/views/admin/deploy_keys/show.html.haml b/app/views/admin/deploy_keys/show.html.haml deleted file mode 100644 index ea361ca4bdb..00000000000 --- a/app/views/admin/deploy_keys/show.html.haml +++ /dev/null @@ -1,35 +0,0 @@ -- page_title @deploy_key.title, "Deploy Keys" -.row - .col-md-4 - .panel.panel-default - .panel-heading - Deploy Key - %ul.well-list - %li - %span.light Title: - %strong= @deploy_key.title - %li - %span.light Created on: - %strong= @deploy_key.created_at.stamp("Aug 21, 2011") - - .panel.panel-default - .panel-heading Projects (#{@deploy_key.deploy_keys_projects.count}) - - if @deploy_key.deploy_keys_projects.any? - %ul.well-list - - @deploy_key.projects.each do |project| - %li - %span - %strong - = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] - .pull-right - = link_to disable_namespace_project_deploy_key_path(project.namespace, project, @deploy_key), data: { confirm: "Are you sure?" }, method: :put, class: "btn-xs btn btn-remove", title: 'Remove deploy key from project' do - %i.fa.fa-times.fa-inverse - - .col-md-8 - %p - %span.light Fingerprint: - %strong= @deploy_key.fingerprint - %pre.well-pre - = @deploy_key.key - .pull-right - = link_to 'Remove', admin_deploy_key_path(@deploy_key), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key" diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index e00b23ad99f..5ce7cdf2f8d 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -11,7 +11,7 @@ = form_tag admin_groups_path, method: :get, class: 'form-inline' do = hidden_field_tag :sort, @sort .form-group - = text_field_tag :name, params[:name], class: "form-control input-mn-300" + = text_field_tag :name, params[:name], class: "form-control" = button_tag "Search", class: "btn submit btn-primary" .pull-right diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 5ecd53cff84..0a354373b9b 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -2,7 +2,7 @@ %h3.page-title Group Membership - if current_user.can_create_group? - %span.pull-right + %span.pull-right.hidden-xs = link_to new_group_path, class: "btn btn-new" do %i.fa.fa-plus New Group @@ -17,18 +17,17 @@ - @group_members.each do |group_member| - group = group_member.group %li - .pull-right + .pull-right.hidden-xs - if can?(current_user, :admin_group, group) = link_to edit_group_path(group), class: "btn-sm btn btn-grouped" do %i.fa.fa-cogs Settings - - if can?(current_user, :destroy_group_member, group_member) - = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Remove user from group' do - %i.fa.fa-sign-out - Leave + = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Leave this group' do + %i.fa.fa-sign-out + Leave - = image_tag group_icon(group), class: "avatar s40 avatar-tile" + = image_tag group_icon(group), class: "avatar s40 avatar-tile hidden-xs" = link_to group, class: 'group-name' do %strong= group.name diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index 812e22373a7..6ec741e4882 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -1,4 +1,9 @@ = form_tag(user_omniauth_callback_path(server['provider_name']), id: 'new_ldap_user' ) do = text_field_tag :username, nil, {class: "form-control top", placeholder: "#{server['label']} Login", autofocus: "autofocus"} = password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"} + - if devise_mapping.rememberable? + .remember-me.checkbox + %label{for: "remember_me"} + = check_box_tag :remember_me, '1', false, id: 'remember_me' + %span Remember me = button_tag "#{server['label']} Sign in", class: "btn-save btn" diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 1da702be384..34a7c00dc43 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -4,8 +4,8 @@ - if event.rm_ref? %strong= event.ref_name - else - = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do - %strong= event.ref_name + %strong + = link_to event.ref_name, namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) at = link_to_project event.project diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index c05d45e0100..f3f0b778539 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -4,7 +4,7 @@ = form_tag explore_groups_path, method: :get, class: 'form-inline form-tiny' do |f| = hidden_field_tag :sort, @sort .form-group - = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input input-mn-300", id: "groups_search" + = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "groups_search" .form-group = button_tag 'Search', class: "btn btn-primary wide" diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index b3963a9d901..82622a58ed2 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -1,7 +1,7 @@ .pull-left = form_tag explore_projects_filter_path, method: :get, class: 'form-inline form-tiny' do |f| .form-group - = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input input-mn-300", id: "projects_search" + = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "projects_search" .form-group = button_tag 'Search', class: "btn btn-primary wide" diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index 56b1948a474..ec39a755f0f 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -40,7 +40,8 @@ - if current_user == user = link_to leave_group_group_members_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do - %i.fa.fa-minus.fa-inverse + = icon("sign-out") + Leave - else = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do %i.fa.fa-minus.fa-inverse diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 903ca877218..a70d1ff0697 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -14,7 +14,7 @@ .clearfix.js-toggle-container = form_tag group_group_members_path(@group), 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 input-mn-300' } + = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input' } = button_tag 'Search', class: 'btn' - if current_user && current_user.can?(:admin_group, @group) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 1678311141e..0687840af39 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -11,7 +11,7 @@ @#{@group.path} - if @group.description.present? .description - = escaped_autolink(@group.description) + = markdown(@group.description, pipeline: :description) %hr = render 'shared/show_aside' diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index b1a57d9824e..dbc68c39bf1 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -15,7 +15,7 @@ %meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'} %meta{name: 'theme-color', content: '#474D57'} - = yield(:meta_tags) + = yield :meta_tags = render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id') = render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id') diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml deleted file mode 100644 index 979755db652..00000000000 --- a/app/views/layouts/_head_panel.html.haml +++ /dev/null @@ -1,42 +0,0 @@ -%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } - .container - %div.app_logo - = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do - = brand_header_logo - %h3 GitLab - %h1.title - = title - - %button.navbar-toggle{type: 'button', data: {target: '.navbar-collapse', toggle: 'collapse'}} - %span.sr-only Toggle navigation - = icon('bars') - - .navbar-collapse.collapse - %ul.nav.navbar-nav - %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') - %li - = link_to help_path, title: 'Help', data: {toggle: 'tooltip', placement: 'bottom'} do - = icon('question-circle') - %li - = link_to explore_root_path, title: 'Explore', data: {toggle: 'tooltip', placement: 'bottom'} do - = icon('globe') - %li - = link_to user_snippets_path(current_user), title: 'Your snippets', data: {toggle: 'tooltip', placement: 'bottom'} do - = icon('clipboard') - - if current_user.is_admin? - %li - = link_to admin_root_path, title: 'Admin area', data: {toggle: 'tooltip', placement: 'bottom'} do - = icon('wrench') - - if current_user.can_create_project? - %li - = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom'} do - = icon('plus') - %li - = link_to profile_path, title: 'Profile settings', data: {toggle: 'tooltip', placement: 'bottom'} do - = icon('cog') - -= render 'shared/outdated_browser' diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index c1283734d25..f17f6fdd91c 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -8,19 +8,13 @@ .collapse-nav = render partial: 'layouts/collapse_button' - if current_user - .sidebar-user - = link_to current_user, class: 'profile-pic', id: 'profile-pic', data: {toggle: 'tooltip', placement: 'top'} do - = image_tag avatar_icon(current_user.email, 60), alt: 'User activity', class: 'avatar avatar s32' + = link_to current_user, class: 'sidebar-user' do + = image_tag avatar_icon(current_user.email, 60), alt: 'User activity', class: 'avatar avatar s32' .username = current_user.username - .logout-holder - = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'top'} do - = icon('sign-out') .content-wrapper .container-fluid .content = render "layouts/flash" .clearfix = yield - -= yield :embedded_scripts diff --git a/app/views/layouts/_public_head_panel.html.haml b/app/views/layouts/_public_head_panel.html.haml deleted file mode 100644 index 8a297566d6c..00000000000 --- a/app/views/layouts/_public_head_panel.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } - .container - %div.app_logo - = link_to explore_root_path, class: "home" do - = brand_header_logo - %h3 GitLab - %h1.title= title - - %button.navbar-toggle{"data-target" => ".navbar-collapse", "data-toggle" => "collapse", type: "button"} - %span.sr-only Toggle navigation - %i.fa.fa-bars - - - unless current_controller?('sessions') - .pull-right.hidden-xs - = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-new append-right-10' - - .navbar-collapse.collapse - %ul.nav.navbar-nav - %li.visible-xs - = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes') - -= render 'shared/outdated_browser' diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index a97feeb1ecd..173033f7eab 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -2,9 +2,14 @@ %html{ lang: "en"} = render "layouts/head" %body{class: "#{app_theme}", :'data-page' => body_data_page} + / Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body. + = yield :scripts_body_top + - if current_user - = render "layouts/head_panel", title: header_title + = render "layouts/header/default", title: header_title - else - = render "layouts/public_head_panel", title: header_title + = render "layouts/header/public", title: header_title = render 'layouts/page', sidebar: sidebar + + = yield :scripts_body diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 5a59c9fd59a..d406f5764a7 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -2,7 +2,7 @@ %html{ lang: "en"} = render "layouts/head" %body.ui_mars.login-page.application - = render "layouts/empty_head_panel" + = render "layouts/header/empty" = render "layouts/broadcast" .container.navless-container .content diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index aa0f3f0a819..2e3a2b16eb7 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -2,7 +2,7 @@ %html{ lang: "en"} = render "layouts/head" %body{class: "#{app_theme} application"} - = render "layouts/head_panel", title: "" if current_user + = render "layouts/header/empty" .container.navless-container = render "layouts/flash" .error-page diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml new file mode 100644 index 00000000000..8b4510d6516 --- /dev/null +++ b/app/views/layouts/header/_default.html.haml @@ -0,0 +1,46 @@ +%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } + .container + .header-logo + = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do + = brand_header_logo + %h3 GitLab + .header-content + %h1.title + = title + + %button.navbar-toggle + %span.sr-only Toggle navigation + = icon('bars') + + .navbar-collapse.collapse + %ul.nav.navbar-nav.pull-right + %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') + %li.hidden-xs + = link_to help_path, title: 'Help', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('question-circle fw') + %li + = link_to explore_root_path, title: 'Explore', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('globe fw') + %li + = link_to user_snippets_path(current_user), title: 'Your snippets', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('clipboard fw') + - if current_user.is_admin? + %li + = link_to admin_root_path, title: 'Admin area', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('wrench fw') + - if current_user.can_create_project? + %li.hidden-xs + = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('plus fw') + %li + = link_to profile_path, title: 'Profile settings', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('cog fw') + %li + = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('sign-out') + += render 'shared/outdated_browser' diff --git a/app/views/layouts/_empty_head_panel.html.haml b/app/views/layouts/header/_empty.html.haml index 358caa3868b..a52a3c8f0ef 100644 --- a/app/views/layouts/_empty_head_panel.html.haml +++ b/app/views/layouts/header/_empty.html.haml @@ -1,4 +1,4 @@ -%header.navbar.navbar-fixed-top.navbar-gitlab +%header.navbar.navbar-fixed-top.navbar-empty .container - %h4.center + .center-logo = image_tag 'logo-white.png', width: 32, height: 32 diff --git a/app/views/layouts/header/_public.html.haml b/app/views/layouts/header/_public.html.haml new file mode 100644 index 00000000000..6a031722aaa --- /dev/null +++ b/app/views/layouts/header/_public.html.haml @@ -0,0 +1,14 @@ +%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } + .container + .header-logo + = link_to explore_root_path, class: "home" do + = brand_header_logo + %h3 GitLab + .header-content + %h1.title= title + + - unless current_controller?('sessions') + .pull-right + = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success btn-sm' + += render 'shared/outdated_browser' diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 172f5197b24..cbcf560d0af 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -56,6 +56,13 @@ Merge Requests %span.count.merge_counter= @project.merge_requests.opened.count + - 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 + = icon('users fw') + %span + Members + - if project_nav_tab? :labels = nav_link(controller: :labels) do = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels', 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 7dd14449def..633c6ae6bfb 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -10,32 +10,27 @@ %ul.project-settings-nav.sidebar-subnav = nav_link(path: 'projects#edit') do = link_to edit_project_path(@project), title: 'Project', class: 'stat-tab tab', data: {placement: 'right'} do - = icon('pencil-square-o') + = icon('pencil-square-o fw') %span Project 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 - = icon('users') - %span - Members = nav_link(controller: :deploy_keys) do = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys', data: {placement: 'right'} do - = icon('key') + = icon('key fw') %span Deploy Keys = nav_link(controller: :hooks) do = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks', data: {placement: 'right'} do - = icon('link') + = icon('link fw') %span Web Hooks = nav_link(controller: :services) do = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services', data: {placement: 'right'} do - = icon('cogs') + = icon('cogs fw') %span Services = nav_link(controller: :protected_branches) do = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches', data: {placement: 'right'} do - = icon('lock') + = icon('lock fw') %span Protected branches diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index 00c7cedce40..ee1b57278b6 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -27,7 +27,7 @@ } .file-stats .deleted-file { color: #B00; - }} + } %body %div.content = yield diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 4aeb9d397d2..44afa33dfe5 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -2,7 +2,13 @@ - header_title project_title(@project) - sidebar "project" unless sidebar -- content_for :embedded_scripts do +- content_for :scripts_body_top do + - if current_user + :javascript + window.project_uploads_path = "#{namespace_project_uploads_path @project.namespace, @project}"; + window.markdown_preview_path = "#{markdown_preview_namespace_project_path(@project.namespace, @project)}"; + +- content_for :scripts_body do = render "layouts/init_auto_complete" if current_user = render template: "layouts/application" diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index a374a662333..12f83aae04b 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -35,7 +35,7 @@ = diff.new_path - elsif diff.new_file %span.new-file - + + + = diff.new_path - else = diff.new_path diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index c30a3f5d79a..a26d4e0c757 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -26,20 +26,23 @@ - if current_user.private_token = text_field_tag "token", current_user.private_token, class: "form-control" %div - = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-primary btn-build-token" + = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default btn-build-token" - else %span You don`t have one yet. Click generate to fix it. - = f.submit 'Generate', class: "btn success btn-build-token" + = f.submit 'Generate', class: "btn btn-default btn-build-token" - unless current_user.ldap_user? - - if current_user.otp_required_for_login - .panel.panel-success - .panel-heading - Two-factor Authentication enabled - .panel-body + .panel.panel-default + .panel-heading + Two-factor Authentication + .panel-body + - if current_user.otp_required_for_login .pull-right = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-close btn-sm', data: { confirm: 'Are you sure?' } + %p.text-success + %strong + Two-factor Authentication is enabled %p If you lose your recovery codes you can %strong @@ -47,11 +50,7 @@ = link_to 'generate new ones', codes_profile_two_factor_auth_path, method: :post, data: { confirm: 'Are you sure?' } invalidating all previous codes. - - else - .panel.panel-default - .panel-heading - Two-factor Authentication - .panel-body + - else %p Increase your account's security by enabling two-factor authentication (2FA). %p diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml index c145a9b7f6d..2c4f0804f0b 100644 --- a/app/views/profiles/applications.html.haml +++ b/app/views/profiles/applications.html.haml @@ -2,37 +2,43 @@ %h3.page-title = page_title %p.light - OAuth2 protocol settings below. + - if user_oauth_applications? + 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 -.oauth-applications - %h3 - Your applications - .pull-right - = link_to 'New Application', new_oauth_application_path, class: 'btn btn-success' - - if @applications.any? - %table.table.table-striped - %thead - %tr - %th Name - %th Callback URL - %th Clients - %th - %th - %tbody - - @applications.each do |application| - %tr{:id => "application_#{application.id}"} - %td= link_to application.name, oauth_application_path(application) - %td - - application.redirect_uri.split.each do |uri| - %div= uri - %td= application.access_tokens.count - %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm' - %td= render 'doorkeeper/applications/delete_form', application: application +- if user_oauth_applications? + .oauth-applications + %h3 + Your applications + .pull-right + = link_to 'New Application', new_oauth_application_path, class: 'btn btn-success' + - if @applications.any? + %table.table.table-striped + %thead + %tr + %th Name + %th Callback URL + %th Clients + %th + %th + %tbody + - @applications.each do |application| + %tr{:id => "application_#{application.id}"} + %td= link_to application.name, oauth_application_path(application) + %td + - application.redirect_uri.split.each do |uri| + %div= uri + %td= application.access_tokens.count + %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm' + %td= render 'doorkeeper/applications/delete_form', application: application .oauth-authorized-applications.prepend-top-20 - %h3 - Authorized applications + - if user_oauth_applications? + %h3 + Authorized applications - if @authorized_tokens.any? %table.table.table-striped diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml index fe5770f45c3..9bbccbc45ea 100644 --- a/app/views/profiles/keys/_key.html.haml +++ b/app/views/profiles/keys/_key.html.haml @@ -3,8 +3,7 @@ = link_to path_to_key(key, is_admin) do %strong= key.title %td - %span - (#{key.fingerprint}) + %code.key-fingerprint= key.fingerprint %td %span.cgray added #{time_ago_with_tooltip(key.created_at)} diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index 8bac22a2e1a..e0ae4d9720f 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -15,7 +15,7 @@ .col-md-8 %p %span.light Fingerprint: - %strong= @key.fingerprint + %code.key-fingerprint= @key.fingerprint %pre.well-pre = @key.key .pull-right diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 62fac46df27..6534afb0e89 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -37,8 +37,11 @@ = f.text_field :email, class: "form-control", required: true - if @user.unconfirmed_email.present? %span.help-block - Please click the link in the confirmation email before continuing, it was sent to - %strong #{@user.unconfirmed_email} + Please click the link in the confirmation email before continuing. It was sent to + = succeed "." do + %strong #{@user.unconfirmed_email} + %p + = link_to "Resend confirmation e-mail", user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post - else %span.help-block We also use email for avatar detection if no avatar is uploaded. diff --git a/app/views/projects/_aside.html.haml b/app/views/projects/_aside.html.haml index 000a40b466d..c9c17110d2b 100644 --- a/app/views/projects/_aside.html.haml +++ b/app/views/projects/_aside.html.haml @@ -56,7 +56,7 @@ - unless @project.empty_repo? .panel.panel-default .panel-heading - = icon("archive fw") + = icon("folder-o fw") Repository .panel-body %ul.nav.nav-pills @@ -94,3 +94,15 @@ = icon("exclamation-triangle fw") Archived project! %p Repository is read-only + + - if current_user + - access = user_max_access_in_project(current_user, @project) + - if access + .light-well.light.prepend-top-20 + %small + You have #{access} access to this project. + - if @project.project_member_by_id(current_user) + %br + = link_to leave_namespace_project_project_members_path(@project.namespace, @project), + data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do + Leave this project diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index f9cdda4a3ba..076afb11a9d 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -5,7 +5,7 @@ .project-home-row.project-home-row-top .project-home-desc - if @project.description.present? - = escaped_autolink(@project.description) + = markdown(@project.description, pipeline: :description) - if can?(current_user, :admin_project, @project) – = link_to 'Edit', edit_namespace_project_path diff --git a/app/views/projects/_issuable_form.html.haml b/app/views/projects/_issuable_form.html.haml index 2292aaaa214..491e2107da4 100644 --- a/app/views/projects/_issuable_form.html.haml +++ b/app/views/projects/_issuable_form.html.haml @@ -15,16 +15,16 @@ - if issuable.is_a?(MergeRequest) %p.help-block - if issuable.work_in_progress? - This merge request is marked a <strong>Work In Progress</strong>. - When it's ready, remove the <code>WIP</code> prefix from the title to allow it to be accepted. + Remove the <code>WIP</code> prefix from the title to allow this + <strong>Work In Progress</strong> merge request to be accepted when it's ready. - else - To prevent this merge request from being accepted before it's ready, - mark it a <strong>Work In Progress</strong> by starting the title with <code>[WIP]</code> or <code>WIP:</code>. + Start the title with <code>[WIP]</code> or <code>WIP:</code> to prevent a + <strong>Work In Progress</strong> merge request from being accepted before it's ready. .form-group.issuable-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: "wiki", referenced_users: true } do = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' .col-sm-12.hint @@ -79,6 +79,24 @@ - if can? current_user, :admin_label, issuable.project = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank +- if issuable.is_a?(MergeRequest) + %hr + - unless @merge_request.persisted? + .form-group + = f.label :source_branch, class: 'control-label' do + %i.fa.fa-code-fork + Source Branch + .col-sm-10 + = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) + %p.help-block + = link_to 'Change source branch', mr_change_branches_path(@merge_request) + .form-group + = f.label :target_branch, class: 'control-label' do + %i.fa.fa-code-fork + Target Branch + .col-sm-10 + = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, { class: 'target_branch select2 span2' }) + .form-actions - if !issuable.project.empty_repo? && (guide_url = contribution_guide_url(issuable.project)) && !issuable.persisted? %p diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index b869fd6e12a..a831481cf80 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -1,13 +1,24 @@ -%ul.nav.nav-tabs - %li.active - = link_to '#md-write-holder', class: 'js-md-write-button' do - Write - %li - = link_to '#md-preview-holder', class: 'js-md-preview-button', - data: { url: markdown_preview_namespace_project_path(@project.namespace, @project) } do - Preview -%div - .md-write-holder - = yield - .md.md-preview-holder.hide - .js-md-preview{class: (preview_class if defined?(preview_class))} +.md-area + .md-header.clearfix + %ul.nav.nav-tabs + %li.active + = link_to '#md-write-holder', class: 'js-md-write-button' do + Write + %li + = link_to '#md-preview-holder', class: 'js-md-preview-button' do + Preview + + - if defined?(referenced_users) && referenced_users + %span.referenced-users.pull-left.hide + = icon('exclamation-triangle') + You are about to add + %strong + %span.js-referenced-users-count 0 + people + to the discussion. Proceed with caution. + + %div + .md-write-holder + = yield + .md.md-preview-holder.hide + .js-md-preview{class: (preview_class if defined?(preview_class))} diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 96f188e4aa7..9c3e1703c89 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -12,8 +12,8 @@ \/ = text_field_tag 'file_name', params[:file_name], placeholder: "File name", required: true, class: 'form-control new-file-name' - .pull-right - = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'form-control' + .pull-right + = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'form-control' .file-content.code %pre.js-edit-mode-pane#editor diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 9b1d03b820e..f7ddf74b4fc 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -6,11 +6,12 @@ = render 'shared/commit_message_container', params: params, placeholder: 'Add new file' - .form-group.branch - = label_tag 'branch', class: 'control-label' do - Branch - .col-sm-10 - = text_field_tag 'new_branch', @ref, class: "form-control" + - unless @project.empty_repo? + .form-group.branch + = label_tag 'branch', class: 'control-label' do + Branch + .col-sm-10 + = text_field_tag 'new_branch', @ref, class: "form-control" = hidden_field_tag 'content', '', id: 'file-content' = render 'projects/commit_button', ref: @ref, diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml index c577dfa8d55..8d66bae8cdf 100644 --- a/app/views/projects/deploy_keys/_deploy_key.html.haml +++ b/app/views/projects/deploy_keys/_deploy_key.html.haml @@ -2,24 +2,20 @@ .pull-right - if @available_keys.include?(deploy_key) = link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-sm', method: :put do - %i.fa.fa-plus + = icon('plus') Enable - else - if deploy_key.destroyed_when_orphaned? && deploy_key.almost_orphaned? = link_to 'Remove', disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), data: { confirm: 'You are going to remove deploy key. Are you sure?'}, method: :put, class: "btn btn-remove delete-key btn-sm pull-right" - else = link_to disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-sm', method: :put do - %i.fa.fa-power-off + = icon('power-off') Disable - - if project = project_for_deploy_key(deploy_key) - = link_to namespace_project_deploy_key_path(project.namespace, project, deploy_key) do - %i.fa.fa-key - %strong= deploy_key.title - - else - %i.fa.fa-key - %strong= deploy_key.title - + = icon('key') + %strong= deploy_key.title + %br + %code.key-fingerprint= deploy_key.fingerprint %p.light.prepend-top-10 - if deploy_key.public? diff --git a/app/views/projects/deploy_keys/show.html.haml b/app/views/projects/deploy_keys/show.html.haml deleted file mode 100644 index 7d44652af72..00000000000 --- a/app/views/projects/deploy_keys/show.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -- page_title @key.title, "Deploy Keys" -%h3.page-title - Deploy key: - = @key.title - %small - created on - = @key.created_at.stamp("Aug 21, 2011") -.back-link - = link_to namespace_project_deploy_keys_path(@project.namespace, @project) do - ← To keys list -%hr -%pre= @key.key -.pull-right - = link_to 'Remove', namespace_project_deploy_key_path(@project.namespace, @project, @key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn-remove btn delete-key" diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index d4b019780f5..99ee23a1ddc 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -10,8 +10,9 @@ - if @commit.parent_ids.present? = view_file_btn(@commit.parent_id, diff_file, project) - elsif diff_file.diff.submodule? - - submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path) - = submodule_link(submodule_item, @commit.id, project.repository) + %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 diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index c09d794ef7f..2765f63c6bc 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -6,7 +6,7 @@ Project settings %hr .panel-body - = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit_project form-horizontal" }, authenticity_token: true do |f| + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit_project form-horizontal fieldset-form" }, authenticity_token: true do |f| %fieldset .form-group.project_name_holder @@ -41,32 +41,40 @@ %legend Features: .form-group - = f.label :issues_enabled, "Issues", class: 'control-label' - .col-sm-10 + .col-sm-offset-2.col-sm-10 .checkbox - = f.check_box :issues_enabled - %span.descr Lightweight issue tracking system for this project + = f.label :issues_enabled do + = f.check_box :issues_enabled + %strong Issues + %br + %span.descr Lightweight issue tracking system for this project .form-group - = f.label :merge_requests_enabled, "Merge Requests", class: 'control-label' - .col-sm-10 + .col-sm-offset-2.col-sm-10 .checkbox - = f.check_box :merge_requests_enabled - %span.descr Submit changes to be merged upstream. + = f.label :merge_requests_enabled do + = f.check_box :merge_requests_enabled + %strong Merge Requests + %br + %span.descr Submit changes to be merged upstream. .form-group - = f.label :wiki_enabled, "Wiki", class: 'control-label' - .col-sm-10 + .col-sm-offset-2.col-sm-10 .checkbox - = f.check_box :wiki_enabled - %span.descr Pages for project documentation + = f.label :wiki_enabled do + = f.check_box :wiki_enabled + %strong Wiki + %br + %span.descr Pages for project documentation .form-group - = f.label :snippets_enabled, "Snippets", class: 'control-label' - .col-sm-10 + .col-sm-offset-2.col-sm-10 .checkbox - = f.check_box :snippets_enabled - %span.descr Share code pastes with others out of git repository + = f.label :snippets_enabled do + = f.check_box :snippets_enabled + %strong Snippets + %br + %span.descr Share code pastes with others out of git repository %fieldset.features %legend diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index 7d7217eb2a8..8d2564be55e 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -10,5 +10,3 @@ $('#issue_assignee_id').val("#{current_user.id}").trigger("change"); e.preventDefault(); }); - - window.project_uploads_path = "#{namespace_project_uploads_path @project.namespace, @project}"; diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index a4e25e5ce88..64d62b45657 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -4,7 +4,7 @@ = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue", disabled: !can?(current_user, :modify_issue, issue) .issue-title - %span.str-truncated + %span.issue-title-text = link_to_gfm issue.title, issue_path(issue), class: "row_title" .issue-labels - issue.labels.each do |label| diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 7d19415a7f4..d44fe486212 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -13,4 +13,7 @@ = paginate @labels, theme: 'gitlab' - else .light-well - .nothing-here-block Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels + - if can? current_user, :admin_label, @project + .nothing-here-block Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels + - else + .nothing-here-block No labels created diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 1c7160bce5f..be73f087449 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -8,5 +8,3 @@ $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change"); e.preventDefault(); }); - - window.project_uploads_path = "#{namespace_project_uploads_path @project.namespace, @project}"; diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 65f5c3d6a19..c16df27ee8f 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,6 +1,6 @@ %li{ class: mr_css_classes(merge_request) } .merge-request-title - %span.str-truncated + %span.merge-request-title-text = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title" .merge-request-labels - merge_request.labels.each do |label| diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index e83b7649928..6792104569b 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -20,12 +20,12 @@ .mr-compare.merge-request %ul.nav.nav-tabs.merge-request-tabs %li.commits-tab - = link_to '#commits', data: {action: 'commits', toggle: 'tab'} do + = link_to url_for(params), data: {target: '#commits', action: 'commits', toggle: 'tab'} do = icon('history') Commits %span.badge= @commits.size %li.diffs-tab - = link_to '#diffs', data: {action: 'diffs', toggle: 'tab'} do + = link_to url_for(params), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do = icon('list-alt') Changes %span.badge= @diffs.size @@ -51,12 +51,10 @@ e.preventDefault(); }); - window.project_uploads_path = "#{namespace_project_uploads_path @project.namespace, @project}"; - :javascript var merge_request merge_request = new MergeRequest({ - action: 'diffs', + action: 'new', diffs_loaded: true, commits_loaded: true }); diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 0d894e360ea..74f8b9950cf 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -22,15 +22,14 @@ %span into %strong.label-branch #{@merge_request.target_branch} - if @merge_request.open? - %span.pull-right - .btn-group - %a.btn.dropdown-toggle{ data: {toggle: :dropdown} } - = icon('download') - Download as - %span.caret - %ul.dropdown-menu - %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) - %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) + .btn-group.btn-group-sm.pull-right + %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} } + = icon('download') + Download as + %span.caret + %ul.dropdown-menu + %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) + %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) = render "projects/merge_requests/show/how_to_merge" = render "projects/merge_requests/show/state_widget" @@ -38,17 +37,17 @@ - if @commits.present? %ul.nav.nav-tabs.merge-request-tabs %li.notes-tab - = link_to '#notes', data: {action: 'notes', toggle: 'tab'} do + = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#notes', action: 'notes', toggle: 'tab'} do = icon('comments') Discussion %span.badge= @merge_request.mr_and_commit_notes.user.count %li.commits-tab - = link_to '#commits', data: {action: 'commits', toggle: 'tab'} do + = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#commits', action: 'commits', toggle: 'tab'} do = icon('history') Commits %span.badge= @commits.size %li.diffs-tab - = link_to '#diffs', data: {source: diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), action: 'diffs', toggle: 'tab'} do + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do = icon('list-alt') Changes %span.badge= @merge_request.diffs.size diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 841d1e1cfe9..fa591b0537e 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -4,9 +4,10 @@ = render 'shared/issuable_search_form', path: namespace_project_merge_requests_path(@project.namespace, @project) - if can? current_user, :write_merge_request, @project - = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-new pull-left", title: "New Merge Request" do - %i.fa.fa-plus - New Merge Request + .pull-left.hidden-xs + = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-new", title: "New Merge Request" do + %i.fa.fa-plus + New Merge Request = render 'shared/issuable_filter', type: :merge_requests .merge-requests-holder = render 'merge_requests' diff --git a/app/views/projects/merge_requests/show/_mr_accept.html.haml b/app/views/projects/merge_requests/show/_mr_accept.html.haml index 906cc11dc67..bfd4ab6f3d8 100644 --- a/app/views/projects/merge_requests/show/_mr_accept.html.haml +++ b/app/views/projects/merge_requests/show/_mr_accept.html.haml @@ -49,7 +49,7 @@ .automerge_widget.cannot_be_merged.hide %h4 - This pull request contains merge conflicts that must be resolved. + This merge request contains merge conflicts that must be resolved. You can try it manually on the %strong = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" @@ -63,14 +63,14 @@ .automerge_widget.work_in_progress.hide %h4 - This request cannot be merged because it is marked as <strong>Work In Progress</strong>. + This merge request cannot be accepted because it is marked as Work In Progress. %p %button.btn.disabled{:type => 'button'} %i.fa.fa-warning Accept Merge Request - When the merge request is ready, remove the "WIP" prefix from the title to allow merging. + When the merge request is ready, remove the "WIP" prefix from the title to allow it to be accepted. .automerge_widget.unchecked %p diff --git a/app/views/projects/merge_requests/show/_state_widget.html.haml b/app/views/projects/merge_requests/show/_state_widget.html.haml index e4c71bfc1be..6396232db22 100644 --- a/app/views/projects/merge_requests/show/_state_widget.html.haml +++ b/app/views/projects/merge_requests/show/_state_widget.html.haml @@ -13,7 +13,7 @@ %h4 Rejected - if @merge_request.closed_event - by #{link_to_member(@project, @merge_request.closed_event.author, avatar: false)} + by #{link_to_member(@project, @merge_request.closed_event.author, avatar: true)} #{time_ago_with_tooltip(@merge_request.closed_event.created_at)} %p Changes were not merged into target branch @@ -21,7 +21,7 @@ %h4 Accepted - if @merge_request.merge_event - by #{link_to_member(@project, @merge_request.merge_event.author, avatar: false)} + by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)} #{time_ago_with_tooltip(@merge_request.merge_event.created_at)} = render "projects/merge_requests/show/remove_source_branch" diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 95b7070ce5c..5650607f31f 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -50,5 +50,3 @@ dateFormat: "yy-mm-dd", onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val())); - - window.project_uploads_path = "#{namespace_project_uploads_path @project.namespace, @project}"; diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 2ada6cb6700..f28b3e9b508 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -5,7 +5,7 @@ = f.hidden_field :noteable_id = f.hidden_field :noteable_type - = render layout: 'projects/md_preview', locals: { preview_class: "note-text" } do + = render layout: 'projects/md_preview', locals: { preview_class: "note-text", referenced_users: true } do = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text' @@ -15,10 +15,7 @@ .error-alert .note-form-actions - .buttons + .buttons.clearfix = f.submit 'Add Comment', class: "btn comment-btn btn-grouped js-comment-button" = yield(:note_actions) %a.btn.grouped.js-close-discussion-note-form Cancel - -:javascript - window.project_uploads_path = "#{namespace_project_uploads_path @project.namespace, @project}"; diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml index 635e4d70941..860a997cff8 100644 --- a/app/views/projects/project_members/_project_member.html.haml +++ b/app/views/projects/project_members/_project_member.html.haml @@ -38,8 +38,9 @@ - if current_user == user - = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: "Leave project?"}, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do - %i.fa.fa-minus.fa-inverse + = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: leave_project_message(@project) }, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do + = icon("sign-out") + Leave - else = link_to namespace_project_project_member_path(@project.namespace, @project, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do %i.fa.fa-minus.fa-inverse diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 6edb92acd4d..162583e4b1d 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -11,7 +11,7 @@ .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 input-mn-300' } + = 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) diff --git a/app/views/projects/update.js.haml b/app/views/projects/update.js.haml index 4f3f4cab8d5..7d9bd08385a 100644 --- a/app/views/projects/update.js.haml +++ b/app/views/projects/update.js.haml @@ -6,4 +6,4 @@ $(".project-edit-errors").html("#{escape_javascript(render('errors'))}"); $('.save-project-loader').hide(); $('.project-edit-container').show(); - $('.project-edit-content .btn-save').enableButton(); + $('.project-edit-content .btn-save').enable(); diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 9fbfa0b1aeb..2a8ceaa2844 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -41,6 +41,3 @@ - else = f.submit 'Create page', class: "btn-create btn" = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, :home), class: "btn btn-cancel" - -:javascript - window.project_uploads_path = "#{namespace_project_uploads_path @project.namespace, @project}"; diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml index 47016daf1f0..5ee70be1ad6 100644 --- a/app/views/search/_form.html.haml +++ b/app/views/search/_form.html.haml @@ -5,7 +5,7 @@ = hidden_field_tag :scope, params[:scope] .search-holder.clearfix .form-group - = search_field_tag :search, params[:search], placeholder: "Search for projects, issues etc", class: "form-control search-text-input input-mn-300", id: "dashboard_search", autofocus: true + = search_field_tag :search, params[:search], placeholder: "Search for projects, issues etc", class: "form-control search-text-input", id: "dashboard_search", autofocus: true = button_tag 'Search', class: "btn btn-primary" - unless params[:snippets].eql? 'true' .pull-right diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index fba69dd0f3f..86921f0a777 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -4,7 +4,8 @@ - blob.data.lines.to_a.size.times do |index| - offset = defined?(first_line_number) ? first_line_number : 1 - i = index + offset - = link_to "#L#{i}", id: "L#{i}", rel: "#L#{i}" do + / We're not using `link_to` because it is too slow once we get to thousands of lines. + %a{href: "#L#{i}", id: "L#{i}", rel: "#L#{i}"} %i.fa.fa-link = i :preserve diff --git a/app/views/shared/_issuable_search_form.html.haml b/app/views/shared/_issuable_search_form.html.haml index 639d203dcd6..58c3de64b77 100644 --- a/app/views/shared/_issuable_search_form.html.haml +++ b/app/views/shared/_issuable_search_form.html.haml @@ -1,6 +1,6 @@ = form_tag(path, method: :get, id: "issue_search_form", class: 'pull-left issue-search-form') do .append-right-10.hidden-xs.hidden-sm - = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input input-mn-300' } + = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input' } = hidden_field_tag :state, params['state'] = hidden_field_tag :scope, params['scope'] = hidden_field_tag :assignee_id, params['assignee_id'] diff --git a/app/views/shared/_project.html.haml b/app/views/shared/_project.html.haml index 722a7f7ce0f..4537f8eec86 100644 --- a/app/views/shared/_project.html.haml +++ b/app/views/shared/_project.html.haml @@ -16,6 +16,3 @@ %span.pull-right.light %i.fa.fa-star = project.star_count - - else - %span.arrow - %i.fa.fa-angle-right diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 6ed45fedfa2..1694818aef6 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -13,7 +13,7 @@ %h3 = @user.name - if @user == current_user - .pull-right + .pull-right.hidden-xs = link_to profile_path, class: 'btn btn-sm' do %i.fa.fa-pencil-square-o Edit Profile settings diff --git a/bin/guard b/bin/guard deleted file mode 100755 index 0c1a532bd01..00000000000 --- a/bin/guard +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env ruby -# -# This file was generated by Bundler. -# -# The application 'guard' is installed as part of a gem, and -# this file is here to facilitate running it. -# - -require 'pathname' -ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", - Pathname.new(__FILE__).realpath) - -require 'rubygems' -require 'bundler/setup' - -load Gem.bin_path('guard', 'guard') diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index fbc7f515f34..f48c99fd901 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -182,12 +182,19 @@ production: &base # Allow login via Twitter, Google, etc. using OmniAuth providers enabled: false + # Uncomment this to automatically sign in with a specific omniauth provider's without + # showing GitLab's sign-in page (default: show the GitLab sign-in page) + # auto_sign_in_with_provider: saml + # CAUTION! # This allows users to login without having a user account first (default: false). # User accounts will be created automatically when authentication was successful. allow_single_sign_on: false # Locks down those users until they have been cleared by the admin (default: true). block_auto_created_users: true + # Look up new users in LDAP servers. If a match is found (same uid), automatically + # link the omniauth identity with the LDAP account. (default: false) + auto_link_ldap_user: false ## Auth providers # Uncomment the following lines and fill in the data of the auth provider you want to use @@ -210,6 +217,15 @@ production: &base # args: { scope: 'api' } } # - { name: 'bitbucket', app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET'} + # - { name: 'saml', + # args: { + # assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + # idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + # idp_sso_target_url: 'https://login.example.com/idp', + # issuer: 'https://gitlab.example.com', + # name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + # } } + @@ -236,6 +252,9 @@ production: &base # 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' + # # Use multipart uploads when file size reaches 100MB, see + # # http://docs.aws.amazon.com/AmazonS3/latest/dev/uploadobjusingmpu.html + # multipart_chunk_size: 104857600 ## GitLab Shell settings gitlab_shell: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 2351ef7b0ce..c2c3c5bfde7 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -87,6 +87,11 @@ end Settings['omniauth'] ||= Settingslogic.new({}) Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? +Settings.omniauth['auto_sign_in_with_provider'] = false if Settings.omniauth['auto_sign_in_with_provider'].nil? +Settings.omniauth['allow_single_sign_on'] = false if Settings.omniauth['allow_single_sign_on'].nil? +Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block_auto_created_users'].nil? +Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil? + Settings.omniauth['providers'] ||= [] Settings['issues_tracker'] ||= {} @@ -169,6 +174,7 @@ Settings.backup['upload'] ||= Settingslogic.new({ 'remote_directory' => nil, 'co 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 # # Git diff --git a/config/initializers/6_rack_profiler.rb b/config/initializers/6_rack_profiler.rb index 38a5fa98dc2..5312fd8e89a 100644 --- a/config/initializers/6_rack_profiler.rb +++ b/config/initializers/6_rack_profiler.rb @@ -3,7 +3,8 @@ if Rails.env.development? # initialization is skipped so trigger it Rack::MiniProfilerRails.initialize!(Rails.application) + Rack::MiniProfiler.config.position = 'right' Rack::MiniProfiler.config.start_hidden = true - Rack::MiniProfiler.config.skip_paths << '/specs' + Rack::MiniProfiler.config.skip_paths << '/teaspoon' end diff --git a/config/initializers/7_omniauth.rb b/config/initializers/7_omniauth.rb index 103aa06ca32..6f1f267bf97 100644 --- a/config/initializers/7_omniauth.rb +++ b/config/initializers/7_omniauth.rb @@ -12,6 +12,8 @@ if Gitlab::LDAP::Config.enabled? end OmniAuth.config.allowed_request_methods = [:post] +#In case of auto sign-in, the GET method is used (users don't get to click on a button) +OmniAuth.config.allowed_request_methods << :get if Gitlab.config.omniauth.auto_sign_in_with_provider.present? OmniAuth.config.before_request_phase do |env| OmniAuth::RequestForgeryProtection.new(env).call end diff --git a/config/routes.rb b/config/routes.rb index bf2cb6421c5..f4a104664f3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,7 +2,6 @@ require 'sidekiq/web' require 'api/api' Gitlab::Application.routes.draw do - mount JasmineRails::Engine => '/specs' if defined?(JasmineRails) use_doorkeeper do controllers applications: 'oauth/applications', authorized_applications: 'oauth/authorized_applications', @@ -166,7 +165,7 @@ Gitlab::Application.routes.draw do end end - resources :deploy_keys, only: [:index, :show, :new, :create, :destroy] + resources :deploy_keys, only: [:index, :new, :create, :destroy] resources :hooks, only: [:index, :create, :destroy] do get :test @@ -422,7 +421,7 @@ Gitlab::Application.routes.draw do end end - resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :show, :new, :create] do + resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do member do put :enable put :disable @@ -450,6 +449,7 @@ Gitlab::Application.routes.draw do resources :merge_requests, constraints: { id: /\d+/ }, except: [:destroy] do member do get :diffs + get :commits post :automerge get :automerge_check get :ci_status diff --git a/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb b/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb new file mode 100644 index 00000000000..6a78294f0b2 --- /dev/null +++ b/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddUserOauthApplicationsToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :user_oauth_applications, :bool, default: true + end +end diff --git a/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb b/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb new file mode 100644 index 00000000000..83e08101407 --- /dev/null +++ b/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb @@ -0,0 +1,5 @@ +class AddAfterSignOutPathForApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :after_sign_out_path, :string + end +end
\ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index 1ab91256406..aea0742cf3b 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: 20150516060434) do +ActiveRecord::Schema.define(version: 20150529150354) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -33,6 +33,8 @@ ActiveRecord::Schema.define(version: 20150516060434) do t.integer "default_project_visibility" t.integer "default_snippet_visibility" t.text "restricted_signup_domains" + t.boolean "user_oauth_applications", default: true + t.string "after_sign_out_path" end create_table "broadcast_messages", force: true do |t| diff --git a/doc/README.md b/doc/README.md index 4e00dceac2b..7a2181edded 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,34 +1,35 @@ -# Documentation - -## User documentation - -- [API](api/README.md) Automate GitLab via a simple and powerful API. -- [Markdown](markdown/markdown.md) GitLab's advanced formatting system. -- [Permissions](permissions/permissions.md) Learn what each role in a project (guest/reporter/developer/master/owner) can do. -- [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat. -- [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. -- [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. -- [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. -- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. - -## Administrator documentation - -- [Install](install/README.md) Requirements, directory structures and installation from source. -- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, LDAP and Twitter. -- [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects. -- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough. -- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed. -- [Security](security/README.md) Learn what you can do to further secure your GitLab instance. -- [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. -- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. -- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. -- [Operations](operations/README.md) Keeping GitLab up and running -- [Log system](logs/logs.md) Log system - -## Contributor documentation - -- [Development](development/README.md) Explains the architecture and the guidelines for shell commands. -- [Legal](legal/README.md) Contributor license agreements. -- [Release](release/README.md) How to make the monthly and security releases. +# Documentation
+
+## User documentation
+
+- [API](api/README.md) Automate GitLab via a simple and powerful API.
+- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
+- [Importing to GitLab](workflow/importing/README.md).
+- [Markdown](markdown/markdown.md) GitLab's advanced formatting system.
+- [Permissions](permissions/permissions.md) Learn what each role in a project (guest/reporter/developer/master/owner) can do.
+- [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat.
+- [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects.
+- [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects.
+- [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.
+
+## Administrator documentation
+
+- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough.
+- [Install](install/README.md) Requirements, directory structures and installation from source.
+- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, LDAP and Twitter.
+- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages.
+- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars.
+- [Log system](logs/logs.md) Log system.
+- [Operations](operations/README.md) Keeping GitLab up and running
+- [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects.
+- [Security](security/README.md) Learn what you can do to further secure your GitLab instance.
+- [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.
+
+## Contributor documentation
+
+- [Development](development/README.md) Explains the architecture and the guidelines for shell commands.
+- [Legal](legal/README.md) Contributor license agreements.
+- [Release](release/README.md) How to make the monthly and security releases.
\ No newline at end of file diff --git a/doc/api/README.md b/doc/api/README.md index f6757b0a6aa..ca58c184543 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -19,6 +19,7 @@ - [Deploy Keys](deploy_keys.md) - [System Hooks](system_hooks.md) - [Groups](groups.md) +- [Namespaces](namespaces.md) ## Clients diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index c1d82ad9576..7b0873a9111 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -221,7 +221,7 @@ If an error occurs, an error number and a message explaining the reason is retur ## Update MR -Updates an existing merge request. You can change branches, title, or even close the MR. +Updates an existing merge request. You can change the target branch, title, or even close the MR. ``` PUT /projects/:id/merge_request/:merge_request_id @@ -231,7 +231,6 @@ Parameters: - `id` (required) - The ID of a project - `merge_request_id` (required) - ID of MR -- `source_branch` - The source branch - `target_branch` - The target branch - `assignee_id` - Assignee user ID - `title` - Title of MR @@ -242,7 +241,6 @@ Parameters: { "id": 1, "target_branch": "master", - "source_branch": "test1", "project_id": 3, "title": "test1", "description": "description1", diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md new file mode 100644 index 00000000000..7b3238441f6 --- /dev/null +++ b/doc/api/namespaces.md @@ -0,0 +1,44 @@ +# Namespaces + +## List namespaces + +Get a list of namespaces. (As user: my namespaces, as admin: all namespaces) + +``` +GET /namespaces +``` + +```json +[ + { + "id": 1, + "path": "user1", + "kind": "user" + }, + { + "id": 2, + "path": "group1", + "kind": "group" + } +] +``` + +You can search for namespaces by name or path, see below. + +## Search for namespace + +Get all namespaces that match your string in their name or path. + +``` +GET /namespaces?search=foobar +``` + +```json +[ + { + "id": 1, + "path": "user1", + "kind": "user" + } +] +``` diff --git a/doc/install/installation.md b/doc/install/installation.md index 1db2b438292..badea4de214 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -241,10 +241,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da # Copy the example Rack attack config sudo -u git -H cp config/initializers/rack_attack.rb.example config/initializers/rack_attack.rb - # Configure Git global settings for git user, useful when editing via web - # Edit user.email according to what is set in gitlab.yml - sudo -u git -H git config --global user.name "GitLab" - sudo -u git -H git config --global user.email "example@example.com" + # Configure Git global settings for git user, used when editing via web editor sudo -u git -H git config --global core.autocrlf input # Configure Redis connection settings diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index d82e1f8b41b..6a0fa4ce015 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -68,6 +68,8 @@ Bitbucket will generate an application ID and secret key for you to use. 1. Save the configuration file. +1. If you're using the omnibus package, reconfigure GitLab (```gitlab-ctl reconfigure```). + 1. Restart GitLab for the changes to take effect. On the sign in page there should now be a Bitbucket icon below the regular sign in form. @@ -80,43 +82,59 @@ To allow projects to be imported directly into GitLab, Bitbucket requires two ex Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and instead requires GitLab to use SSH and identify itself using your GitLab server's SSH key. -### Step 1: Known hosts +### Step 1: Public key -To allow GitLab to connect to Bitbucket over SSH, you need to add 'bitbucket.org' to your GitLab server's known SSH hosts. Take the following steps to do so: +To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa.pub`, which will expand to `/home/git/.ssh/bitbucket_rsa.pub` in most configurations. -1. Manually connect to 'bitbucket.org' over SSH, while logged in as the `git` account that GitLab will use: +If you have that file in place, you're all set and should see the "Import projects from Bitbucket" option enabled. If you don't, do the following: + +1. Create a new SSH key: ```sh - ssh git@bitbucket.org + sudo -u git -H ssh-keygen ``` -1. Verify the RSA key fingerprint you'll see in the response matches the one in the [Bitbucket documentation](https://confluence.atlassian.com/display/BITBUCKET/Use+the+SSH+protocol+with+Bitbucket#UsetheSSHprotocolwithBitbucket-KnownhostorBitbucket'spublickeyfingerprints) (the specific IP address doesn't matter): + When asked `Enter file in which to save the key` specify the correct path, eg. `/home/git/.ssh/bitbucket_rsa`. + Make sure to use an **empty passphrase**. + +1. Configure SSH client to use your new key: + + Open the SSH configuration file of the git user. ```sh - The authenticity of host 'bitbucket.org (207.223.240.182)' can't be established. - RSA key fingerprint is 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40. - Are you sure you want to continue connecting (yes/no)? + sudo editor /home/git/.ssh/config ``` -1. If the fingerprint matches, type `yes` to continue connecting and have 'bitbucket.org' be added to your known hosts. + Add a host configuration for `bitbucket.org`. + + ```sh + Host bitbucket.org + IdentityFile ~/.ssh/bitbucket_rsa + User git + ``` -1. Your GitLab server is now able to connect to Bitbucket over SSH. Continue to step 2: +### Step 2: Known hosts -### Step 2: Public key +To allow GitLab to connect to Bitbucket over SSH, you need to add 'bitbucket.org' to your GitLab server's known SSH hosts. Take the following steps to do so: -To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa.pub`, which will expand to `/home/git/.ssh/bitbucket_rsa.pub` in most configurations. +1. Manually connect to 'bitbucket.org' over SSH, while logged in as the `git` account that GitLab will use: -If you have that file in place, you're all set and should see the "Import projects from Bitbucket" option enabled. If you don't, do the following: + ```sh + sudo -u git -H ssh bitbucket.org + ``` -1. Create a new SSH key: +1. Verify the RSA key fingerprint you'll see in the response matches the one in the [Bitbucket documentation](https://confluence.atlassian.com/display/BITBUCKET/Use+the+SSH+protocol+with+Bitbucket#UsetheSSHprotocolwithBitbucket-KnownhostorBitbucket'spublickeyfingerprints) (the specific IP address doesn't matter): ```sh - sudo -u git -H ssh-keygen + The authenticity of host 'bitbucket.org (207.223.240.182)' can't be established. + RSA key fingerprint is 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40. + Are you sure you want to continue connecting (yes/no)? ``` - When asked `Enter file in which to save the key` specify the correct path, eg. `/home/git/.ssh/bitbucket_rsa`. - Make sure to use an **empty passphrase**. +1. If the fingerprint matches, type `yes` to continue connecting and have 'bitbucket.org' be added to your known hosts. + +1. Your GitLab server is now able to connect to Bitbucket over SSH. -2. Restart GitLab to allow it to find the new public key. +1. Restart GitLab to allow it to find the new public key. You should now see the "Import projects from Bitbucket" option on the New Project page enabled. diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index b67f793c591..904d5d7fee2 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -6,6 +6,13 @@ The first time a user signs in with LDAP credentials, GitLab will create a new G GitLab user attributes such as nickname and email will be copied from the LDAP user entry. +## Security + +GitLab assumes that LDAP users are not able to change their LDAP 'mail', 'email' or 'userPrincipalName' attribute. +An LDAP user who is allowed to change their email on the LDAP server can [take over any account](#enabling-ldap-sign-in-for-existing-gitlab-users) on your GitLab server. + +We recommend against using GitLab LDAP integration if your LDAP users are allowed to change their 'mail', 'email' or 'userPrincipalName' attribute on the LDAP server. + ## Configuring GitLab for LDAP integration To enable GitLab LDAP integration you need to add your LDAP server settings in `/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`. diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 24f7b4bb4b4..8e2a602ec35 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -75,6 +75,7 @@ Now we can choose one or more of the Supported Providers below to continue confi - [Google](google.md) - [Shibboleth](shibboleth.md) - [Twitter](twitter.md) +- [SAML](saml.md) ## Enable OmniAuth for an Existing User diff --git a/doc/integration/saml.md b/doc/integration/saml.md new file mode 100644 index 00000000000..a8cc5c8f74a --- /dev/null +++ b/doc/integration/saml.md @@ -0,0 +1,77 @@ +# SAML OmniAuth Provider + +GitLab can be configured to act as a SAML 2.0 Service Provider (SP). This allows GitLab to consume assertions from a SAML 2.0 Identity Provider (IdP) such as Microsoft ADFS to authenticate users. + +First configure SAML 2.0 support in GitLab, then register the GitLab application in your SAML IdP: + +1. Make sure GitLab is configured with HTTPS. See [Using HTTPS](../install/installation.md#using-https) for instructions. + +1. On your GitLab server, open the configuration file. + + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For instalations from source: + + ```sh + cd /home/git/gitlab + + sudo -u git -H editor config/gitlab.yml + ``` + +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "saml", + args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://login.example.com/idp', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + } + } + ] + ``` + + For installations from source: + + ```yaml + - { name: 'saml', + args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://login.example.com/idp', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + } } + ``` + +1. Change the value for 'assertion_consumer_service_url' to match the HTTPS endpoint of GitLab (append 'users/auth/saml/callback' to the HTTPS URL of your GitLab installation to generate the correct value). + +1. Change the values of 'idp_cert_fingerprint', 'idp_sso_target_url', 'name_identifier_format' to match your IdP. Check [the omniauth-saml documentation](https://github.com/PracticallyGreen/omniauth-saml) for details on these options. + +1. Change the value of 'issuer' to a unique name, which will identify the application to the IdP. + +1. Restart GitLab for the changes to take effect. + +1. Register the GitLab SP in your SAML 2.0 IdP, using the application name specified in 'issuer'. + +To ease configuration, most IdP accept a metadata URL for the application to provide configuration information to the IdP. To build the metadata URL for GitLab, append 'users/auth/saml/metadata' to the HTTPS URL of your GitLab installation, for instance: + ``` + https://gitlab.example.com/users/auth/saml/metadata + ``` + +At a minimum the IdP *must* provide a claim containing the user's email address, using claim name 'email' or 'mail'. The email will be used to automatically generate the GitLab username. GitLab will also use claims with name 'name', 'first_name', 'last_name' (see [the omniauth-saml gem](https://github.com/PracticallyGreen/omniauth-saml/blob/master/lib/omniauth/strategies/saml.rb) for supported claims). + +On the sign in page there should now be a SAML button below the regular sign in form. Click the icon to begin the authentication process. If everything goes well the user will be returned to GitLab and will be signed in. + diff --git a/doc/operations/README.md b/doc/operations/README.md index f1456c6c8e2..6a35dab7b6c 100644 --- a/doc/operations/README.md +++ b/doc/operations/README.md @@ -2,3 +2,4 @@ - [Sidekiq MemoryKiller](sidekiq_memory_killer.md) - [Cleaning up Redis sessions](cleaning_up_redis_sessions.md) +- [Understanding Unicorn and unicorn-worker-killer](unicorn.md) diff --git a/doc/operations/unicorn.md b/doc/operations/unicorn.md new file mode 100644 index 00000000000..31b432cd411 --- /dev/null +++ b/doc/operations/unicorn.md @@ -0,0 +1,86 @@ +# Understanding Unicorn and unicorn-worker-killer + +## Unicorn + +GitLab uses [Unicorn](http://unicorn.bogomips.org/), a pre-forking Ruby web +server, to handle web requests (web browsers and Git HTTP clients). Unicorn is +a daemon written in Ruby and C that can load and run a Ruby on Rails +application; in our case the Rails application is GitLab Community Edition or +GitLab Enterprise Edition. + +Unicorn has a multi-process architecture to make better use of available CPU +cores (processes can run on different cores) and to have stronger fault +tolerance (most failures stay isolated in only one process and cannot take down +GitLab entirely). On startup, the Unicorn 'master' process loads a clean Ruby +environment with the GitLab application code, and then spawns 'workers' which +inherit this clean initial environment. The 'master' never handles any +requests, that is left to the workers. The operating system network stack +queues incoming requests and distributes them among the workers. + +In a perfect world, the master would spawn its pool of workers once, and then +the workers handle incoming web requests one after another until the end of +time. In reality, worker processes can crash or time out: if the master notices +that a worker takes too long to handle a request it will terminate the worker +process with SIGKILL ('kill -9'). No matter how the worker process ended, the +master process will replace it with a new 'clean' process again. Unicorn is +designed to be able to replace 'crashed' workers without dropping user +requests. + +This is what a Unicorn worker timeout looks like in `unicorn_stderr.log`. The +master process has PID 56227 below. + +``` +[2015-06-05T10:58:08.660325 #56227] ERROR -- : worker=10 PID:53009 timeout (61s > 60s), killing +[2015-06-05T10:58:08.699360 #56227] ERROR -- : reaped #<Process::Status: pid 53009 SIGKILL (signal 9)> worker=10 +[2015-06-05T10:58:08.708141 #62538] INFO -- : worker=10 spawned pid=62538 +[2015-06-05T10:58:08.708824 #62538] INFO -- : worker=10 ready +``` + +### Tunables + +The main tunables for Unicorn are the number of worker processes and the +request timeout after which the Unicorn master terminates a worker process. +See the [omnibus-gitlab Unicorn settings +documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md) +if you want to adjust these settings. + +## unicorn-worker-killer + +GitLab has memory leaks. These memory leaks manifest themselves in long-running +processes, such as Unicorn workers. (The Unicorn master process is not known to +leak memory, probably because it does not handle user requests.) + +To make these memory leaks manageable, GitLab comes with the +[unicorn-worker-killer gem](https://github.com/kzk/unicorn-worker-killer). This +gem [monkey-patches](http://en.wikipedia.org/wiki/Monkey_patch) the Unicorn +workers to do a memory self-check after every 16 requests. If the memory of the +Unicorn worker exceeds a pre-set limit then the worker process exits. The +Unicorn master then automatically replaces the worker process. + +This is a robust way to handle memory leaks: Unicorn is designed to handle +workers that 'crash' so no user requests will be dropped. The +unicorn-worker-killer gem is designed to only terminate a worker process _in +between requests_, so no user requests are affected. + +This is what a Unicorn worker memory restart looks like in unicorn_stderr.log. +You see that worker 4 (PID 125918) is inspecting itself and decides to exit. +The threshold memory value was 254802235 bytes, about 250MB. With GitLab this +threshold is a random value between 200 and 250 MB. The master process (PID +117565) then reaps the worker process and spawns a new 'worker 4' with PID +127549. + +``` +[2015-06-05T12:07:41.828374 #125918] WARN -- : #<Unicorn::HttpServer:0x00000002734770>: worker (pid: 125918) exceeds memory limit (256413696 bytes > 254802235 bytes) +[2015-06-05T12:07:41.828472 #125918] WARN -- : Unicorn::WorkerKiller send SIGQUIT (pid: 125918) alive: 23 sec (trial 1) +[2015-06-05T12:07:42.025916 #117565] INFO -- : reaped #<Process::Status: pid 125918 exit 0> worker=4 +[2015-06-05T12:07:42.034527 #127549] INFO -- : worker=4 spawned pid=127549 +[2015-06-05T12:07:42.035217 #127549] INFO -- : worker=4 ready +``` + +One other thing that stands out in the log snippet above, taken from +Gitlab.com, is that 'worker 4' was serving requests for only 23 seconds. This +is a normal value for our current GitLab.com setup and traffic. + +The high frequency of Unicorn memory restarts on some GitLab sites can be a +source of confusion for administrators. Usually they are a [red +herring](http://en.wikipedia.org/wiki/Red_herring). diff --git a/doc/raketasks/maintenance.md b/doc/raketasks/maintenance.md index 41a994f3f68..2aca91d5371 100644 --- a/doc/raketasks/maintenance.md +++ b/doc/raketasks/maintenance.md @@ -47,7 +47,6 @@ Git: /usr/bin/git Runs the following rake tasks: -- `gitlab:env:check` - `gitlab:gitlab_shell:check` - `gitlab:sidekiq:check` - `gitlab:app:check` @@ -147,7 +146,7 @@ Do you want to continue (yes/no)? yes ## Clear redis cache -If for some reason the dashboard shows wrong information you might want to +If for some reason the dashboard shows wrong information you might want to clear Redis' cache. For Omnibus-packages: diff --git a/doc/release/monthly.md b/doc/release/monthly.md index eb97f3cd7f6..57d904ff389 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -71,9 +71,14 @@ Xth: (1 working day before the 22nd) - [ ] Update GitLab.com with the stable version (#LINK) - [ ] Update ci.gitLab.com with the stable version (#LINK) -22nd: +22nd before 12AM CET: + +Release before 12AM CET / 3AM PST, to make sure the majority of our users +get the new version on the 22nd and there is sufficient time in the European +workday to quickly fix any issues. - [ ] Release CE, EE and CI (#LINK) +- [ ] Schedule a second tweet of the release announcement at 6PM CET / 9AM PST ``` diff --git a/doc/workflow/2fa.png b/doc/workflow/2fa.png Binary files differnew file mode 100644 index 00000000000..bbf415210d5 --- /dev/null +++ b/doc/workflow/2fa.png diff --git a/doc/workflow/2fa_auth.png b/doc/workflow/2fa_auth.png Binary files differnew file mode 100644 index 00000000000..4a4fbe68984 --- /dev/null +++ b/doc/workflow/2fa_auth.png diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 0fca68f364e..70a8179c8eb 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -1,17 +1,16 @@ -# Workflow - -- [Feature branch workflow](workflow.md) -- [Project forking workflow](forking_workflow.md) -- [Project Features](project_features.md) -- [Authorization for merge requests](authorization_for_merge_requests.md) -- [Groups](groups.md) -- [Labels](labels.md) -- [GitLab Flow](gitlab_flow.md) -- [Notifications](notifications.md) -- [Migrating from SVN to GitLab](migrating_from_svn.md) -- [Project importing from GitHub to GitLab](import_projects_from_github.md) -- [Project importing from GitLab.com to your private GitLab instance](import_projects_from_gitlab_com.md) -- [Protected branches](protected_branches.md) -- [Change your time zone](timezone.md) -- [Keyboard shortcuts](shortcuts.md) -- [Web Editor](web_editor.md)
\ No newline at end of file +# Workflow
+
+- [Authorization for merge requests](authorization_for_merge_requests.md)
+- [Change your time zone](timezone.md)
+- [Feature branch workflow](workflow.md)
+- [GitLab Flow](gitlab_flow.md)
+- [Groups](groups.md)
+- [Keyboard shortcuts](shortcuts.md)
+- [Labels](labels.md)
+- [Notifications](notifications.md)
+- [Project Features](project_features.md)
+- [Project forking workflow](forking_workflow.md)
+- [Protected branches](protected_branches.md)
+- [Two-factor Authentication (2FA)](two_factor_authentication.md)
+- [Web Editor](web_editor.md)
+- ["Work In Progress" Merge Requests](wip_merge_requests.md)
\ No newline at end of file diff --git a/doc/workflow/import_projects_from_github.md b/doc/workflow/import_projects_from_github.md deleted file mode 100644 index 8644b4ffc73..00000000000 --- a/doc/workflow/import_projects_from_github.md +++ /dev/null @@ -1,13 +0,0 @@ -# Project importing from GitHub to GitLab - -You can import your existing GitHub projects to GitLab. But keep in mind that it is possible only if -GitHub support is enabled on your GitLab instance. You can read more about GitHub support [here](http://doc.gitlab.com/ce/integration/github.html) -To get to the importer page you need to go to "New project" page. - - - -Click on the "Import project from GitHub" link and you will be redirected to GitHub for permission to access your projects. After accepting, you'll be automatically redirected to the importer. - - - -To import a project, you can simple click "Add". The importer will import your repository and issues. Once the importer is done, a new GitLab project will be created with your imported data.
\ No newline at end of file diff --git a/doc/workflow/importing/README.md b/doc/workflow/importing/README.md new file mode 100644 index 00000000000..2b2e9037425 --- /dev/null +++ b/doc/workflow/importing/README.md @@ -0,0 +1,6 @@ +# Migrating projects to a GitLab instance
+
+1. [Bitbucket](import_projects_from_bitbucket.md)
+2. [GitHub](import_projects_from_github.md)
+3. [GitLab.com](import_projects_from_gitlab_com.md)
+4. [SVN](migrating_from_svn.md)
diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png Binary files differnew file mode 100644 index 00000000000..df55a081803 --- /dev/null +++ b/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png Binary files differnew file mode 100644 index 00000000000..5253889d251 --- /dev/null +++ b/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png Binary files differnew file mode 100644 index 00000000000..ffa87ce5b2e --- /dev/null +++ b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png Binary files differnew file mode 100644 index 00000000000..0e08703f421 --- /dev/null +++ b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png diff --git a/doc/workflow/github_importer/importer.png b/doc/workflow/importing/github_importer/importer.png Binary files differindex 57636717571..57636717571 100644 --- a/doc/workflow/github_importer/importer.png +++ b/doc/workflow/importing/github_importer/importer.png diff --git a/doc/workflow/github_importer/new_project_page.png b/doc/workflow/importing/github_importer/new_project_page.png Binary files differindex 002f22d81d7..002f22d81d7 100644 --- a/doc/workflow/github_importer/new_project_page.png +++ b/doc/workflow/importing/github_importer/new_project_page.png diff --git a/doc/workflow/gitlab_importer/importer.png b/doc/workflow/importing/gitlab_importer/importer.png Binary files differindex d2a286d8cac..d2a286d8cac 100644 --- a/doc/workflow/gitlab_importer/importer.png +++ b/doc/workflow/importing/gitlab_importer/importer.png diff --git a/doc/workflow/gitlab_importer/new_project_page.png b/doc/workflow/importing/gitlab_importer/new_project_page.png Binary files differindex 5e239208e1e..5e239208e1e 100644 --- a/doc/workflow/gitlab_importer/new_project_page.png +++ b/doc/workflow/importing/gitlab_importer/new_project_page.png diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md new file mode 100644 index 00000000000..1e9825e2e10 --- /dev/null +++ b/doc/workflow/importing/import_projects_from_bitbucket.md @@ -0,0 +1,26 @@ +# Import your project from Bitbucket to GitLab
+
+It takes just a few steps to import your existing Bitbucket projects to GitLab. But keep in mind that it is possible only if Bitbucket support is enabled on your GitLab instance. You can read more about Bitbucket support [here](doc/integration/bitbucket.md).
+
+* Sign in to GitLab.com and go to your dashboard
+
+* Click on "New project"
+
+
+
+* Click on the "Bitbucket" button
+
+
+
+* Grant GitLab access to your Bitbucket account
+
+
+
+* Click on the projects that you'd like to import or "Import all projects"
+
+
+
+A new GitLab project will be created with your imported data.
+
+### Note
+Milestones and wiki pages are not imported from Bitbucket.
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md new file mode 100644 index 00000000000..aad2c63817d --- /dev/null +++ b/doc/workflow/importing/import_projects_from_github.md @@ -0,0 +1,18 @@ +# Import your project from GitHub to GitLab
+
+It takes just a couple of steps to import your existing GitHub projects to GitLab. Keep in mind that it is possible only if
+GitHub support is enabled on your GitLab instance. You can read more about GitHub support [here](http://doc.gitlab.com/ce/integration/github.html)
+
+* Sign in to GitLab.com and go to your dashboard.
+* To get to the importer page, you need to go to the "New project" page.
+
+
+
+* Click on the "Import project from GitHub" link and you will be redirected to GitHub for permission to access your projects. After accepting, you'll be automatically redirected to the importer.
+
+
+
+* To import a project, you can simple click "Add". The importer will import your repository and issues. Once the importer is done, a new GitLab project will be created with your imported data.
+
+### Note
+When you import your projects from GitHub, it is not possible to keep your labels and milestones and issue numbers won't match. We are working on improving this in the near future.
\ No newline at end of file diff --git a/doc/workflow/import_projects_from_gitlab_com.md b/doc/workflow/importing/import_projects_from_gitlab_com.md index f4c4e955d46..f4c4e955d46 100644 --- a/doc/workflow/import_projects_from_gitlab_com.md +++ b/doc/workflow/importing/import_projects_from_gitlab_com.md diff --git a/doc/workflow/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md index 485db4834e9..485db4834e9 100644 --- a/doc/workflow/migrating_from_svn.md +++ b/doc/workflow/importing/migrating_from_svn.md diff --git a/doc/workflow/two_factor_authentication.md b/doc/workflow/two_factor_authentication.md new file mode 100644 index 00000000000..7c45d23c99d --- /dev/null +++ b/doc/workflow/two_factor_authentication.md @@ -0,0 +1,67 @@ +# Two-factor Authentication (2FA) + +Two-factor Authentication (2FA) provides an additional level of security to your +GitLab account. Once enabled, in addition to supplying your username and +password to login, you'll be prompted for a code generated by an application on +your phone. + +By enabling 2FA, the only way someone other than you can log into your account +is to know your username and password *and* have access to your phone. + +## Enabling 2FA + +**In GitLab:** + +1. Log in to your GitLab account. +1. Go to your **Profile Settings**. +1. Go to **Account**. +1. Click **Enable Two-factor Authentication**. + + + +**On your phone:** + +1. Install a compatible application. We recommend [Google Authenticator] +\(proprietary\) or [FreeOTP] \(open source\). +1. In the application, add a new entry in one of two ways: + * Scan the code with your phone's camera to add the entry automatically. + * Enter the details provided to add the entry manually. + +**In GitLab:** + +1. Enter the six-digit pin number from the entry on your phone into the **Pin + code** field. +1. Click **Submit**. + +If the pin you entered was correct, you'll see a message indicating that +Two-factor Authentication has been enabled, and you'll be presented with a list +of recovery codes. + +## Recovery Codes + +Should you ever lose access to your phone, you can use one of the ten provided +backup codes to login to your account. We suggest copying or printing them for +storage in a safe place. **Each code can be used only once** to log in to your +account. + +If you lose the recovery codes or just want to generate new ones, you can do so +from the **Profile Settings** > **Acount** page where you first enabled 2FA. + +## Logging in with 2FA Enabled + +Logging in with 2FA enabled is only slightly different than a normal login. +Enter your username and password credentials as you normally would, and you'll +be presented with a second prompt for an authentication code. Enter the pin from +your phone's application or a recovery code to log in. + + + +## Disabling 2FA + +1. Log in to your GitLab account. +1. Go to your **Profile Settings**. +1. Go to **Acount**. +1. Click **Disable Two-factor Authentication**. + +[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en +[FreeOTP]: https://fedorahosted.org/freeotp/
\ No newline at end of file diff --git a/doc/workflow/wip_merge_requests.md b/doc/workflow/wip_merge_requests.md new file mode 100644 index 00000000000..46035a5e6b6 --- /dev/null +++ b/doc/workflow/wip_merge_requests.md @@ -0,0 +1,13 @@ +# "Work In Progress" Merge Requests + +To prevent merge requests from accidentally being accepted before they're completely ready, GitLab blocks the "Accept" button for merge requests that have been marked a **Work In Progress**. + + + +To mark a merge request a Work In Progress, simply start its title with `[WIP]` or `WIP:`. + + + +To allow a Work In Progress merge request to be accepted again when it's ready, simply remove the `WIP` prefix. + + diff --git a/doc/workflow/wip_merge_requests/blocked_accept_button.png b/doc/workflow/wip_merge_requests/blocked_accept_button.png Binary files differnew file mode 100644 index 00000000000..4791e5de972 --- /dev/null +++ b/doc/workflow/wip_merge_requests/blocked_accept_button.png diff --git a/doc/workflow/wip_merge_requests/mark_as_wip.png b/doc/workflow/wip_merge_requests/mark_as_wip.png Binary files differnew file mode 100644 index 00000000000..8fa83a201ac --- /dev/null +++ b/doc/workflow/wip_merge_requests/mark_as_wip.png diff --git a/doc/workflow/wip_merge_requests/unmark_as_wip.png b/doc/workflow/wip_merge_requests/unmark_as_wip.png Binary files differnew file mode 100644 index 00000000000..d45e68f31c5 --- /dev/null +++ b/doc/workflow/wip_merge_requests/unmark_as_wip.png diff --git a/doc_styleguide.md b/doc_styleguide.md index 670af765f3a..db30a737f14 100644 --- a/doc_styleguide.md +++ b/doc_styleguide.md @@ -12,6 +12,7 @@ This styleguide recommends best practices to improve documentation and to keep i * Be brief and clear. +* Whenever it applies, add documents in alphabetical order. ## When adding images to a document diff --git a/docker/README.md b/docker/README.md index 2e533ae9dd5..46b21348364 100644 --- a/docker/README.md +++ b/docker/README.md @@ -107,7 +107,7 @@ The directories on data container are: ### Configure GitLab -These container uses the official Omnibus GitLab distribution, so all configuration is done in the unique configuration file `/etc/gitlab/gitlab.rb`. +This container uses the official Omnibus GitLab distribution, so all configuration is done in the unique configuration file `/etc/gitlab/gitlab.rb`. To access GitLab configuration, you can start an interactive command line in a new container using the shared data volume container, you will be able to browse the 3 directories and use your favorite text editor: @@ -122,7 +122,7 @@ You can find all available options in [Omnibus GitLab documentation](https://git ### Upgrade GitLab with app and data images -To updgrade GitLab to new versions, stop running container, create new docker image and container from that image. +To upgrade GitLab to new versions, stop running container, create new docker image and container from that image. It Assumes that you're upgrading from 7.8.1 to 7.10.1 and you're in the updated GitLab repo root directory: @@ -141,10 +141,12 @@ sudo docker rmi gitlab-app:7.8.1 ### Publish images to Dockerhub -Login to Dockerhub with `sudo docker login` and run the following (replace '7.9.2' with the version you're using and 'Sytse Sijbrandij' with your name): +- Ensure the containers are running +- Login to Dockerhub with `sudo docker login` +- Run the following (replace '7.9.2' with the version you're using and 'Sytse Sijbrandij' with your name): ```bash -sudo docker commit -m "Initial commit" -a "Sytse Sijbrandij" gitlab-app:7.10.1 sytse/gitlab-app:7.10.1 +sudo docker commit -m "Initial commit" -a "Sytse Sijbrandij" gitlab-app sytse/gitlab-app:7.10.1 sudo docker push sytse/gitlab-app:7.10.1 sudo docker commit -m "Initial commit" -a "Sytse Sijbrandij" gitlab_data sytse/gitlab_data sudo docker push sytse/gitlab_data diff --git a/features/admin/deploy_keys.feature b/features/admin/deploy_keys.feature index 9df47eb51fd..33439cd1e85 100644 --- a/features/admin/deploy_keys.feature +++ b/features/admin/deploy_keys.feature @@ -8,11 +8,6 @@ Feature: Admin Deploy Keys When I visit admin deploy keys page Then I should see all public deploy keys - Scenario: Deploy Keys show - When I visit admin deploy keys page - And I click on first deploy key - Then I should see deploy key details - Scenario: Deploy Keys new When I visit admin deploy keys page And I click 'New Deploy Key' diff --git a/features/dashboard/group.feature b/features/dashboard/group.feature index cf4b8d7283b..e3c01db2ebb 100644 --- a/features/dashboard/group.feature +++ b/features/dashboard/group.feature @@ -24,7 +24,8 @@ Feature: Dashboard Group When I visit dashboard groups page Then I should see group "Owned" in group list Then I should see group "Guest" in group list - Then I should not see the "Leave" button for group "Owned" + When I click on the "Leave" button for group "Owned" + Then I should see the "Can not leave message" @javascript Scenario: Guest should be able to leave from group diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index 05faad4e645..8661ea98c20 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -35,6 +35,11 @@ Feature: Project Active Tab Then the active main tab should be Merge Requests And no other main tabs should be active + Scenario: On Project Members + Given I visit my project's members page + Then the active main tab should be Members + And no other main tabs should be active + Scenario: On Project Wiki Given I visit my project's wiki page Then the active main tab should be Wiki @@ -49,13 +54,6 @@ Feature: Project Active Tab # Sub Tabs: Settings - Scenario: On Project Settings/Team - Given I visit my project's settings page - And I click the "Team" tab - Then the active sub nav should be Team - And no other sub navs should be active - And the active main tab should be Settings - Scenario: On Project Settings/Edit Given I visit my project's settings page And I click the "Edit" tab diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 7a831901607..eb091c291e9 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -207,3 +207,11 @@ Feature: Project Merge Requests Then I should see that I am subscribed When I click button "Unsubscribe" Then I should see that I am unsubscribed + + @javascript + Scenario: I can change the target branch + Given I visit merge request page "Bug NS-04" + And I click link "Edit" for the merge request + When I click the "Target branch" dropdown + And I select a new target branch + Then I should see new target branch changes diff --git a/features/steps/admin/deploy_keys.rb b/features/steps/admin/deploy_keys.rb index fb0b611762e..844837d177d 100644 --- a/features/steps/admin/deploy_keys.rb +++ b/features/steps/admin/deploy_keys.rb @@ -14,17 +14,6 @@ class Spinach::Features::AdminDeployKeys < Spinach::FeatureSteps end end - step 'I click on first deploy key' do - click_link DeployKey.are_public.first.title - end - - step 'I should see deploy key details' do - deploy_key = DeployKey.are_public.first - current_path.should == admin_deploy_key_path(deploy_key) - page.should have_content(deploy_key.title) - page.should have_content(deploy_key.key) - end - step 'I visit admin deploy key page' do visit admin_deploy_key_path(deploy_key) end diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index 8508b2a8096..bb1f2f444f9 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -23,8 +23,8 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps step 'I see prefilled new Merge Request page' do current_path.should == new_namespace_project_merge_request_path(@project.namespace, @project) find("#merge_request_target_project_id").value.should == @project.id.to_s - find("#merge_request_source_branch").value.should == "fix" - find("#merge_request_target_branch").value.should == "master" + find("input#merge_request_source_branch").value.should == "fix" + find("input#merge_request_target_branch").value.should == "master" end step 'user with name "John Doe" joined project "Shop"' do diff --git a/features/steps/dashboard/group.rb b/features/steps/dashboard/group.rb index 8384df2fb59..aeea49320ff 100644 --- a/features/steps/dashboard/group.rb +++ b/features/steps/dashboard/group.rb @@ -60,4 +60,8 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps page.should have_content "Samurai" page.should have_content "Tokugawa Shogunate" end + + step 'I should see the "Can not leave message"' do + page.should have_content "You can not leave Owned group because you're the last owner" + end end diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index b8f79f70ca4..d16e6bbea57 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -7,21 +7,23 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I change my profile info' do - fill_in "user_skype", with: "testskype" - fill_in "user_linkedin", with: "testlinkedin" - fill_in "user_twitter", with: "testtwitter" - fill_in "user_website_url", with: "testurl" - fill_in "user_location", with: "Ukraine" - click_button "Save changes" + fill_in 'user_skype', with: 'testskype' + fill_in 'user_linkedin', with: 'testlinkedin' + fill_in 'user_twitter', with: 'testtwitter' + fill_in 'user_website_url', with: 'testurl' + fill_in 'user_location', with: 'Ukraine' + fill_in 'user_bio', with: 'I <3 GitLab' + click_button 'Save changes' @user.reload end step 'I should see new profile info' do - @user.skype.should == 'testskype' - @user.linkedin.should == 'testlinkedin' - @user.twitter.should == 'testtwitter' - @user.website_url.should == 'testurl' - find("#user_location").value.should == "Ukraine" + expect(@user.skype).to eq 'testskype' + expect(@user.linkedin).to eq 'testlinkedin' + expect(@user.twitter).to eq 'testtwitter' + expect(@user.website_url).to eq 'testurl' + expect(@user.bio).to eq 'I <3 GitLab' + find('#user_location').value.should == 'Ukraine' end step 'I change my avatar' do @@ -166,7 +168,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I click on my profile picture' do - click_link 'profile-pic' + find(:css, '.sidebar-user').click end step 'I should see my user page' do diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index c888e82e207..30d53333b78 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -69,14 +69,13 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'I visit big commit page' do - Commit::DIFF_SAFE_FILES = 20 + stub_const('Commit::DIFF_SAFE_FILES', 20) visit namespace_project_commit_path(@project.namespace, @project, sample_big_commit.id) end step 'I see big commit warning' do page.should have_content sample_big_commit.message page.should have_content "Too many changes" - Commit::DIFF_SAFE_FILES = 100 end step 'I visit a commit with an image that changed' do diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 48bb316e203..4ca7cf5e5fe 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -305,6 +305,20 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps fill_in 'issue_search', with: "Fe" end + step 'I click the "Target branch" dropdown' do + first('.target_branch').click + end + + step 'I select a new target branch' do + select "feature", from: "merge_request_target_branch" + click_button 'Save' + end + + step 'I should see new target branch changes' do + page.should have_content 'From fix into feature' + page.should have_content 'Target branch changed from master to feature' + end + def merge_request @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05") end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index d9401bd540c..46493876805 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -227,6 +227,10 @@ module SharedPaths visit namespace_project_merge_requests_path(@project.namespace, @project) end + step "I visit my project's members page" do + visit namespace_project_project_members_path(@project.namespace, @project) + end + step "I visit my project's wiki page" do visit namespace_project_wiki_path(@project.namespace, @project, :home) end diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index c5aed19331c..71fe89f634f 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -28,6 +28,10 @@ module SharedProjectTab ensure_active_main_tab('Issues') end + step 'the active main tab should be Members' do + ensure_active_main_tab('Members') + end + step 'the active main tab should be Merge Requests' do ensure_active_main_tab('Merge Requests') end diff --git a/features/support/env.rb b/features/support/env.rb index f34302721ed..d4a878ea4ce 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -9,7 +9,6 @@ end ENV['RAILS_ENV'] = 'test' require './config/environment' -require 'rspec' require 'rspec/expectations' require 'sidekiq/testing/inline' diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 23270b1c0f4..f4efb651eb6 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -62,7 +62,7 @@ module API sha = params[:sha] commit = user_project.commit(sha) not_found! 'Commit' unless commit - notes = Note.where(commit_id: commit.id) + notes = Note.where(commit_id: commit.id).order(:created_at) present paginate(notes), with: Entities::CommitNote end diff --git a/lib/api/files.rb b/lib/api/files.rb index e0ea6d7dd1d..c7b30cf2f07 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -3,6 +3,26 @@ module API class Files < Grape::API before { authenticate! } + helpers do + def commit_params(attrs) + { + file_path: attrs[:file_path], + current_branch: attrs[:branch_name], + target_branch: attrs[:branch_name], + commit_message: attrs[:commit_message], + file_content: attrs[:content], + file_content_encoding: attrs[:encoding] + } + end + + def commit_response(attrs) + { + file_path: attrs[:file_path], + branch_name: attrs[:branch_name], + } + end + end + resource :projects do # Get file from repository # File content is Base64 encoded @@ -73,17 +93,11 @@ module API required_attributes! [:file_path, :branch_name, :content, :commit_message] attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding] - branch_name = attrs.delete(:branch_name) - file_path = attrs.delete(:file_path) - result = ::Files::CreateService.new(user_project, current_user, attrs, branch_name, file_path).execute + result = ::Files::CreateService.new(user_project, current_user, commit_params(attrs)).execute if result[:status] == :success status(201) - - { - file_path: file_path, - branch_name: branch_name - } + commit_response(attrs) else render_api_error!(result[:message], 400) end @@ -105,17 +119,11 @@ module API required_attributes! [:file_path, :branch_name, :content, :commit_message] attrs = attributes_for_keys [:file_path, :branch_name, :content, :commit_message, :encoding] - branch_name = attrs.delete(:branch_name) - file_path = attrs.delete(:file_path) - result = ::Files::UpdateService.new(user_project, current_user, attrs, branch_name, file_path).execute + result = ::Files::UpdateService.new(user_project, current_user, commit_params(attrs)).execute if result[:status] == :success status(200) - - { - file_path: file_path, - branch_name: branch_name - } + commit_response(attrs) else http_status = result[:http_status] || 400 render_api_error!(result[:message], http_status) @@ -138,17 +146,11 @@ module API required_attributes! [:file_path, :branch_name, :commit_message] attrs = attributes_for_keys [:file_path, :branch_name, :commit_message] - branch_name = attrs.delete(:branch_name) - file_path = attrs.delete(:file_path) - result = ::Files::DeleteService.new(user_project, current_user, attrs, branch_name, file_path).execute + result = ::Files::DeleteService.new(user_project, current_user, commit_params(attrs)).execute if result[:status] == :success status(200) - - { - file_path: file_path, - branch_name: branch_name - } + commit_response(attrs) else render_api_error!(result[:message], 400) end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index f768c750402..e88b6e31775 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -62,7 +62,7 @@ module API delete ":id" do group = find_group(params[:id]) authorize! :admin_group, group - group.destroy + DestroyGroupService.new(group, current_user).execute end # Transfer a project to the Group namespace diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 2216a12a87a..d835dce2ded 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -137,7 +137,6 @@ module API # Parameters: # id (required) - The ID of a project # merge_request_id (required) - ID of MR - # source_branch - The source branch # target_branch - The target branch # assignee_id - Assignee user ID # title - Title of MR @@ -148,10 +147,15 @@ module API # PUT /projects/:id/merge_request/:merge_request_id # put ":id/merge_request/:merge_request_id" do - attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :state_event, :description] + attrs = attributes_for_keys [:target_branch, :assignee_id, :title, :state_event, :description] merge_request = user_project.merge_requests.find(params[:merge_request_id]) authorize! :modify_merge_request, merge_request + # Ensure source_branch is not specified + if params[:source_branch].present? + render_api_error!('Source branch cannot be changed', 400) + end + # Validate label names in advance if (errors = validate_label_params(params)).any? render_api_error!({ labels: errors }, 400) diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb index b90ed6af5fb..50d3729449e 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -1,10 +1,7 @@ module API # namespaces API class Namespaces < Grape::API - before do - authenticate! - authenticated_as_admin! - end + before { authenticate! } resource :namespaces do # Get a namespaces list @@ -12,7 +9,11 @@ module API # Example Request: # GET /namespaces get do - @namespaces = Namespace.all + @namespaces = if current_user.admin + Namespace.all + else + current_user.namespaces + end @namespaces = @namespaces.search(params[:search]) if params[:search].present? @namespaces = paginate @namespaces diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index b69aebf9fe1..6fa2079d1a8 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -46,7 +46,8 @@ module Backup connection = ::Fog::Storage.new(connection_settings) directory = connection.directories.get(remote_directory) - if directory.files.create(key: tar_file, body: File.open(tar_file), public: false) + if directory.files.create(key: tar_file, body: File.open(tar_file), public: false, + multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size) $progress.puts "done".green else puts "uploading backup to #{remote_directory} failed".red diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 530f9d93de4..172d4902add 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -244,6 +244,16 @@ module Gitlab end end + # Check if such directory exists in repositories. + # + # Usage: + # exists?('gitlab') + # exists?('gitlab/cookies.git') + # + def exists?(dir_name) + File.exists?(full_path(dir_name)) + end + protected def gitlab_shell_path @@ -264,10 +274,6 @@ module Gitlab File.join(repos_path, dir_name) end - def exists?(dir_name) - File.exists?(full_path(dir_name)) - end - def gitlab_shell_projects_path File.join(gitlab_shell_path, 'bin', 'gitlab-projects') end diff --git a/lib/gitlab/gitorious_import.rb b/lib/gitlab/gitorious_import.rb new file mode 100644 index 00000000000..8d0132a744c --- /dev/null +++ b/lib/gitlab/gitorious_import.rb @@ -0,0 +1,5 @@ +module Gitlab + module GitoriousImport + GITORIOUS_HOST = "https://gitorious.org" + end +end diff --git a/lib/gitlab/gitorious_import/client.rb b/lib/gitlab/gitorious_import/client.rb index 1fa89dba448..99fe5bdebfc 100644 --- a/lib/gitlab/gitorious_import/client.rb +++ b/lib/gitlab/gitorious_import/client.rb @@ -1,7 +1,5 @@ module Gitlab module GitoriousImport - GITORIOUS_HOST = "https://gitorious.org" - class Client attr_reader :repo_list diff --git a/lib/gitlab/gitorious_import/repository.rb b/lib/gitlab/gitorious_import/repository.rb index f702797dc6e..c88f1ae358d 100644 --- a/lib/gitlab/gitorious_import/repository.rb +++ b/lib/gitlab/gitorious_import/repository.rb @@ -1,7 +1,5 @@ module Gitlab module GitoriousImport - GITORIOUS_HOST = "https://gitorious.org" - Repository = Struct.new(:full_name) do def id Digest::SHA1.hexdigest(full_name) diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 5db1566f55d..fa9c0975bb8 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -57,6 +57,9 @@ module Gitlab pipeline = HTML::Pipeline.new(filters) context = { + # SanitizationFilter + pipeline: options[:pipeline], + # EmojiFilter asset_root: Gitlab.config.gitlab.url, asset_host: Gitlab::Application.config.asset_host, diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index be4d26af0fc..a84bacd3d4f 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -25,12 +25,18 @@ module Gitlab ERB::Util.html_escape_once(html) end - # Don't look for references in text nodes that are children of these - # elements. - IGNORE_PARENTS = %w(pre code a style).to_set + def ignore_parents + @ignore_parents ||= begin + # Don't look for references in text nodes that are children of these + # elements. + parents = %w(pre code a style) + parents << 'blockquote' if context[:ignore_blockquotes] + parents.to_set + end + end def ignored_ancestry?(node) - has_ancestor?(node, IGNORE_PARENTS) + has_ancestor?(node, ignore_parents) end def project diff --git a/lib/gitlab/markdown/sanitization_filter.rb b/lib/gitlab/markdown/sanitization_filter.rb index 88781fea0c8..74b3a8d274f 100644 --- a/lib/gitlab/markdown/sanitization_filter.rb +++ b/lib/gitlab/markdown/sanitization_filter.rb @@ -8,33 +8,54 @@ module Gitlab # Extends HTML::Pipeline::SanitizationFilter with a custom whitelist. class SanitizationFilter < HTML::Pipeline::SanitizationFilter def whitelist - whitelist = super + # Descriptions are more heavily sanitized, allowing only a few elements. + # See http://git.io/vkuAN + if pipeline == :description + whitelist = LIMITED + whitelist[:elements] -= %w(pre code img ol ul li) + else + whitelist = super + end + + customize_whitelist(whitelist) + + whitelist + end + private + + def pipeline + context[:pipeline] || :default + end + + def customized?(transformers) + transformers.last.source_location[0] == __FILE__ + end + + def customize_whitelist(whitelist) # Only push these customizations once - unless customized?(whitelist[:transformers]) - # Allow code highlighting - whitelist[:attributes]['pre'] = %w(class) - whitelist[:attributes]['span'] = %w(class) + return if customized?(whitelist[:transformers]) - # Allow table alignment - whitelist[:attributes]['th'] = %w(style) - whitelist[:attributes]['td'] = %w(style) + # Allow code highlighting + whitelist[:attributes]['pre'] = %w(class) + whitelist[:attributes]['span'] = %w(class) - # Allow span elements - whitelist[:elements].push('span') + # Allow table alignment + whitelist[:attributes]['th'] = %w(style) + whitelist[:attributes]['td'] = %w(style) - # Remove `rel` attribute from `a` elements - whitelist[:transformers].push(remove_rel) + # Allow span elements + whitelist[:elements].push('span') - # Remove `class` attribute from non-highlight spans - whitelist[:transformers].push(clean_spans) - end + # Remove `rel` attribute from `a` elements + whitelist[:transformers].push(remove_rel) + + # Remove `class` attribute from non-highlight spans + whitelist[:transformers].push(clean_spans) whitelist end - private - def remove_rel lambda do |env| if env[:node_name] == 'a' @@ -53,10 +74,6 @@ module Gitlab end end end - - def customized?(transformers) - transformers.last.source_location[0] == __FILE__ - end end end end diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index ba5caed6131..c4971b5bcc6 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -46,6 +46,10 @@ module Gitlab def gl_user @user ||= find_by_uid_and_provider + if auto_link_ldap_user? + @user ||= find_or_create_ldap_user + end + if signup_enabled? @user ||= build_new_user end @@ -55,6 +59,46 @@ module Gitlab protected + def find_or_create_ldap_user + return unless ldap_person + + # If a corresponding person exists with same uid in a LDAP server, + # set up a Gitlab user with dual LDAP and Omniauth identities. + if user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn.downcase, ldap_person.provider) + # Case when a LDAP user already exists in Gitlab. Add the Omniauth identity to existing account. + user.identities.build(extern_uid: auth_hash.uid, provider: auth_hash.provider) + else + # No account in Gitlab yet: create it and add the LDAP identity + user = build_new_user + user.identities.new(provider: ldap_person.provider, extern_uid: ldap_person.dn) + end + + user + end + + def auto_link_ldap_user? + Gitlab.config.omniauth.auto_link_ldap_user + end + + def creating_linked_ldap_user? + auto_link_ldap_user? && ldap_person + end + + def ldap_person + return @ldap_person if defined?(@ldap_person) + + # looks for a corresponding person with same uid in any of the configured LDAP providers + @ldap_person = Gitlab::LDAP::Config.providers.find do |provider| + adapter = Gitlab::LDAP::Adapter.new(provider) + + Gitlab::LDAP::Person.find_by_uid(auth_hash.uid, adapter) + end + end + + def ldap_config + Gitlab::LDAP::Config.new(ldap_person.provider) if ldap_person + end + def needs_blocking? new? && block_after_signup? end @@ -64,7 +108,11 @@ module Gitlab end def block_after_signup? - Gitlab.config.omniauth.block_auto_created_users + if creating_linked_ldap_user? + ldap_config.block_auto_created_users + else + Gitlab.config.omniauth.block_auto_created_users + end end def auth_hash=(auth_hash) @@ -84,10 +132,19 @@ module Gitlab end def user_attributes + # Give preference to LDAP for sensitive information when creating a linked account + if creating_linked_ldap_user? + username = ldap_person.username + email = ldap_person.email.first + else + username = auth_hash.username + email = auth_hash.email + end + { name: auth_hash.name, - username: ::Namespace.clean_path(auth_hash.username), - email: auth_hash.email, + username: ::Namespace.clean_path(username), + email: email, password: auth_hash.password, password_confirmation: auth_hash.password, password_automatically_set: true diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index e35f848fa6e..e836b05ff25 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -1,7 +1,7 @@ module Gitlab # Extract possible GFM references from an arbitrary String for further processing. class ReferenceExtractor - attr_accessor :project, :current_user, :references + attr_accessor :project, :current_user def initialize(project, current_user = nil) @project = project @@ -9,48 +9,31 @@ module Gitlab end def analyze(text) - @_text = text.dup + references.clear + @text = markdown.render(text.dup) end - def users - result = pipeline_result(:user) - result.uniq + %i(user label issue merge_request snippet commit commit_range).each do |type| + define_method("#{type}s") do + references[type] + end end - def labels - result = pipeline_result(:label) - result.uniq - end - - def issues - # TODO (rspeicher): What about external issues? - - result = pipeline_result(:issue) - result.uniq - end - - def merge_requests - result = pipeline_result(:merge_request) - result.uniq - end + private - def snippets - result = pipeline_result(:snippet) - result.uniq + def markdown + @markdown ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML, GitlabMarkdownHelper::MARKDOWN_OPTIONS) end - def commits - result = pipeline_result(:commit) - result.uniq - end + def references + @references ||= Hash.new do |references, type| + type = type.to_sym + return references[type] if references.has_key?(type) - def commit_ranges - result = pipeline_result(:commit_range) - result.uniq + references[type] = pipeline_result(type).uniq + end end - private - # Instantiate and call HTML::Pipeline with a single reference filter type, # returning the result # @@ -65,11 +48,12 @@ module Gitlab project: project, current_user: current_user, # We don't actually care about the links generated - only_path: true + only_path: true, + ignore_blockquotes: true } pipeline = HTML::Pipeline.new([filter], context) - result = pipeline.call(@_text) + result = pipeline.call(@text) result[:references][filter_type] end diff --git a/lib/gitlab/satellite/files/delete_file_action.rb b/lib/gitlab/satellite/files/delete_file_action.rb deleted file mode 100644 index 0d37b9dea85..00000000000 --- a/lib/gitlab/satellite/files/delete_file_action.rb +++ /dev/null @@ -1,50 +0,0 @@ -require_relative 'file_action' - -module Gitlab - module Satellite - class DeleteFileAction < FileAction - # Deletes file and creates a new commit for it - # - # Returns false if committing the change fails - # Returns false if pushing from the satellite to bare repo failed or was rejected - # Returns true otherwise - def commit!(content, commit_message) - in_locked_and_timed_satellite do |repo| - prepare_satellite!(repo) - - # create target branch in satellite at the corresponding commit from bare repo - repo.git.checkout({ raise: true, timeout: true, b: true }, ref, "origin/#{ref}") - - # update the file in the satellite's working dir - file_path_in_satellite = File.join(repo.working_dir, file_path) - - # Prevent relative links - unless safe_path?(file_path_in_satellite) - Gitlab::GitLogger.error("FileAction: Relative path not allowed") - return false - end - - File.delete(file_path_in_satellite) - - # add removed file - repo.remove(file_path_in_satellite) - - # commit the changes - # will raise CommandFailed when commit fails - repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) - - - # push commit back to bare repo - # will raise CommandFailed when push fails - repo.git.push({ raise: true, timeout: true }, :origin, ref) - - # everything worked - true - end - rescue Grit::Git::CommandFailed => ex - Gitlab::GitLogger.error(ex.message) - false - end - end - end -end diff --git a/lib/gitlab/satellite/files/edit_file_action.rb b/lib/gitlab/satellite/files/edit_file_action.rb deleted file mode 100644 index 3cb9c0b5ecb..00000000000 --- a/lib/gitlab/satellite/files/edit_file_action.rb +++ /dev/null @@ -1,68 +0,0 @@ -require_relative 'file_action' - -module Gitlab - module Satellite - # GitLab server-side file update and commit - class EditFileAction < FileAction - # Updates the files content and creates a new commit for it - # - # Returns false if the ref has been updated while editing the file - # Returns false if committing the change fails - # Returns false if pushing from the satellite to bare repo failed or was rejected - # Returns true otherwise - def commit!(content, commit_message, encoding, new_branch = nil) - in_locked_and_timed_satellite do |repo| - prepare_satellite!(repo) - - # create target branch in satellite at the corresponding commit from bare repo - begin - repo.git.checkout({ raise: true, timeout: true, b: true }, ref, "origin/#{ref}") - rescue Grit::Git::CommandFailed => ex - log_and_raise(CheckoutFailed, ex.message) - end - - # update the file in the satellite's working dir - file_path_in_satellite = File.join(repo.working_dir, file_path) - - # Prevent relative links - unless safe_path?(file_path_in_satellite) - Gitlab::GitLogger.error("FileAction: Relative path not allowed") - return false - end - - # Write file - write_file(file_path_in_satellite, content, encoding) - - # commit the changes - # will raise CommandFailed when commit fails - begin - repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) - rescue Grit::Git::CommandFailed => ex - log_and_raise(CommitFailed, ex.message) - end - - - target_branch = new_branch.present? ? "#{ref}:#{new_branch}" : ref - - # push commit back to bare repo - # will raise CommandFailed when push fails - begin - repo.git.push({ raise: true, timeout: true }, :origin, target_branch) - rescue Grit::Git::CommandFailed => ex - log_and_raise(PushFailed, ex.message) - end - - # everything worked - true - end - end - - private - - def log_and_raise(errorClass, message) - Gitlab::GitLogger.error(message) - raise(errorClass, message) - end - end - end -end diff --git a/lib/gitlab/satellite/files/file_action.rb b/lib/gitlab/satellite/files/file_action.rb deleted file mode 100644 index 6446b14568a..00000000000 --- a/lib/gitlab/satellite/files/file_action.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Gitlab - module Satellite - class FileAction < Action - attr_accessor :file_path, :ref - - def initialize(user, project, ref, file_path) - super user, project - @file_path = file_path - @ref = ref - end - - def safe_path?(path) - File.absolute_path(path) == path - end - - def write_file(abs_file_path, content, file_encoding = 'text') - if file_encoding == 'base64' - File.open(abs_file_path, 'wb') { |f| f.write(Base64.decode64(content)) } - else - File.open(abs_file_path, 'w') { |f| f.write(content) } - end - end - end - end -end diff --git a/lib/gitlab/satellite/files/new_file_action.rb b/lib/gitlab/satellite/files/new_file_action.rb deleted file mode 100644 index 724dfa0d042..00000000000 --- a/lib/gitlab/satellite/files/new_file_action.rb +++ /dev/null @@ -1,67 +0,0 @@ -require_relative 'file_action' - -module Gitlab - module Satellite - class NewFileAction < FileAction - # Updates the files content and creates a new commit for it - # - # Returns false if the ref has been updated while editing the file - # Returns false if committing the change fails - # Returns false if pushing from the satellite to bare repo failed or was rejected - # Returns true otherwise - def commit!(content, commit_message, encoding, new_branch = nil) - in_locked_and_timed_satellite do |repo| - prepare_satellite!(repo) - - # create target branch in satellite at the corresponding commit from bare repo - current_ref = - if @project.empty_repo? - # skip this step if we want to add first file to empty repo - Satellite::PARKING_BRANCH - else - repo.git.checkout({ raise: true, timeout: true, b: true }, ref, "origin/#{ref}") - ref - end - - file_path_in_satellite = File.join(repo.working_dir, file_path) - dir_name_in_satellite = File.dirname(file_path_in_satellite) - - # Prevent relative links - unless safe_path?(file_path_in_satellite) - Gitlab::GitLogger.error("FileAction: Relative path not allowed") - return false - end - - # Create dir if not exists - FileUtils.mkdir_p(dir_name_in_satellite) - - # Write file - write_file(file_path_in_satellite, content, encoding) - - # add new file - repo.add(file_path_in_satellite) - - # commit the changes - # will raise CommandFailed when commit fails - repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) - - target_branch = if new_branch.present? && !@project.empty_repo? - "#{ref}:#{new_branch}" - else - "#{current_ref}:#{ref}" - end - - # push commit back to bare repo - # will raise CommandFailed when push fails - repo.git.push({ raise: true, timeout: true }, :origin, target_branch) - - # everything worked - true - end - rescue Grit::Git::CommandFailed => ex - Gitlab::GitLogger.error(ex.message) - false - end - end - end -end diff --git a/lib/gitlab/upgrader.rb b/lib/gitlab/upgrader.rb index 0570c2fbeb5..cf040971c6e 100644 --- a/lib/gitlab/upgrader.rb +++ b/lib/gitlab/upgrader.rb @@ -43,10 +43,15 @@ module Gitlab end def latest_version_raw + git_tags = fetch_git_tags + git_tags = git_tags.select { |version| version =~ /v\d+\.\d+\.\d+\Z/ } + git_versions = git_tags.map { |tag| Gitlab::VersionInfo.parse(tag.match(/v\d+\.\d+\.\d+/).to_s) } + "v#{git_versions.sort.last.to_s}" + end + + def fetch_git_tags remote_tags, _ = Gitlab::Popen.popen(%W(git ls-remote --tags https://gitlab.com/gitlab-org/gitlab-ce.git)) - git_tags = remote_tags.split("\n").grep(/tags\/v#{current_version.major}/) - git_tags = git_tags.select { |version| version =~ /v\d\.\d\.\d\Z/ } - last_tag = git_tags.last.match(/v\d\.\d\.\d/).to_s + remote_tags.split("\n").grep(/tags\/v#{current_version.major}/) end def update_commands diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb index 7dcecc2ecf6..2f7aff03c2a 100644 --- a/lib/redcarpet/render/gitlab_html.rb +++ b/lib/redcarpet/render/gitlab_html.rb @@ -10,6 +10,8 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML @options = options.dup @options.reverse_merge!( + # Handled further down the line by Gitlab::Markdown::SanitizationFilter + escape_html: false, project: @template.instance_variable_get("@project") ) diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index b066a1a6935..946902e2f6d 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -35,13 +35,14 @@ pid_path="$app_root/tmp/pids" socket_path="$app_root/tmp/sockets" web_server_pid_path="$pid_path/unicorn.pid" sidekiq_pid_path="$pid_path/sidekiq.pid" +shell_path="/bin/bash" # Read configuration variable file if it is present test -f /etc/default/gitlab && . /etc/default/gitlab # Switch to the app_user if it is not he/she who is running the script. if [ "$USER" != "$app_user" ]; then - eval su - "$app_user" -c $(echo \")$0 "$@"$(echo \"); exit; + eval su - "$app_user" -s $shell_path -c $(echo \")$0 "$@"$(echo \"); exit; fi # Switch to the gitlab path, exit on failure. diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example index 9951bacedf5..cf7f4198cbf 100755 --- a/lib/support/init.d/gitlab.default.example +++ b/lib/support/init.d/gitlab.default.example @@ -29,3 +29,8 @@ web_server_pid_path="$pid_path/unicorn.pid" # sidekiq_pid_path defines the path in which to create the pid file for sidekiq # The default is "$pid_path/sidekiq.pid" sidekiq_pid_path="$pid_path/sidekiq.pid" + +# shell_path defines the path of shell for "$app_user" in case you are using +# shell other than "bash" +# The default is "/bin/bash" +shell_path="/bin/bash" diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 1a6303b6c82..75bd41f2838 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -1,7 +1,6 @@ namespace :gitlab do desc "GITLAB | Check the configuration of GitLab and its environment" - task check: %w{gitlab:env:check - gitlab:gitlab_shell:check + task check: %w{gitlab:gitlab_shell:check gitlab:sidekiq:check gitlab:ldap:check gitlab:app:check} @@ -14,6 +13,7 @@ namespace :gitlab do warn_user_is_not_gitlab start_checking "GitLab" + check_git_config check_database_config_exists check_database_is_not_sqlite check_migrations_are_up @@ -38,6 +38,36 @@ namespace :gitlab do # Checks ######################## + def check_git_config + print "Git configured with autocrlf=input? ... " + + options = { + "core.autocrlf" => "input" + } + + correct_options = options.map do |name, value| + run(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value + end + + if correct_options.all? + puts "yes".green + else + print "Trying to fix Git error automatically. ..." + + if auto_fix_git_config(options) + puts "Success".green + else + puts "Failed".red + try_fixing_it( + sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"") + ) + for_more_information( + see_installation_guide_section "GitLab" + ) + end + end + end + def check_database_config_exists print "Database config exists? ... " @@ -298,58 +328,6 @@ namespace :gitlab do end end - - - namespace :env do - desc "GITLAB | Check the configuration of the environment" - task check: :environment do - warn_user_is_not_gitlab - start_checking "Environment" - - check_gitlab_git_config - - finished_checking "Environment" - end - - - # Checks - ######################## - - def check_gitlab_git_config - print "Git configured for #{gitlab_user} user? ... " - - options = { - "user.name" => "GitLab", - "user.email" => Gitlab.config.gitlab.email_from, - "core.autocrlf" => "input" - } - correct_options = options.map do |name, value| - run(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value - end - - if correct_options.all? - puts "yes".green - else - print "Trying to fix Git error automatically. ..." - if auto_fix_git_config(options) - puts "Success".green - else - puts "Failed".red - try_fixing_it( - sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global user.name \"#{options["user.name"]}\""), - sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global user.email \"#{options["user.email"]}\""), - sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"") - ) - for_more_information( - see_installation_guide_section "GitLab" - ) - end - end - end - end - - - namespace :gitlab_shell do desc "GITLAB | Check the configuration of GitLab Shell" task check: :environment do diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index e835d6cb9b7..afdaba11cb0 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -59,6 +59,9 @@ namespace :gitlab do # Launch installation process system(*%W(bin/install)) + + # (Re)create hooks + system(*%W(bin/create-hooks)) end # Required for debian packaging with PKGR: Setup .ssh/environment with diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake index 14a130be2ca..c95b6540ebc 100644 --- a/lib/tasks/gitlab/task_helpers.rake +++ b/lib/tasks/gitlab/task_helpers.rake @@ -118,9 +118,9 @@ namespace :gitlab do # Returns true if all subcommands were successfull (according to their exit code) # Returns false if any or all subcommands failed. def auto_fix_git_config(options) - if !@warned_user_not_gitlab && options['user.email'] != 'example@example.com' # default email should be overridden? + if !@warned_user_not_gitlab command_success = options.map do |name, value| - system(%W(#{Gitlab.config.git.bin_path} config --global #{name} #{value})) + system(*%W(#{Gitlab.config.git.bin_path} config --global #{name} #{value})) end command_success.all? diff --git a/lib/tasks/jasmine.rake b/lib/tasks/jasmine.rake index 9e2cceffa19..ac307a9e929 100644 --- a/lib/tasks/jasmine.rake +++ b/lib/tasks/jasmine.rake @@ -7,6 +7,6 @@ task jasmine: ['jasmine:ci'] namespace :jasmine do task :ci do - Rake::Task['spec:javascript'].invoke + Rake::Task['teaspoon'].invoke end end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb new file mode 100644 index 00000000000..edc1c63a0aa --- /dev/null +++ b/spec/features/groups_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +feature 'Group' do + describe 'description' do + let(:group) { create(:group) } + let(:path) { group_path(group) } + + before do + login_as(:admin) + end + + it 'parses Markdown' do + group.update_attribute(:description, 'This is **my** group') + visit path + expect(page).to have_css('.description > p > strong') + end + + it 'passes through html-pipeline' do + group.update_attribute(:description, 'This group is the :poop:') + visit path + expect(page).to have_css('.description > p > img') + end + + it 'sanitizes unwanted tags' do + group.update_attribute(:description, '# Group Description') + visit path + expect(page).not_to have_css('.description h1') + end + + it 'permits `rel` attribute on links' do + group.update_attribute(:description, 'https://google.com/') + visit path + expect(page).to have_css('.description a[rel]') + end + end +end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index ee1b3bf749d..902968cebcb 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -18,11 +18,13 @@ require 'erb' # -> `gfm_with_options` helper # -> HTML::Pipeline # -> Sanitize +# -> RelativeLink # -> Emoji # -> Table of Contents # -> Autolinks # -> Rinku (http, https, ftp) # -> Other schemes +# -> ExternalLink # -> References # -> TaskList # -> `html_safe` diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index cae11be7cdd..f8eea70ec4a 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -1,32 +1,57 @@ require 'spec_helper' -describe "Projects", feature: true, js: true do - before { login_as :user } +feature 'Project' do + describe 'description' do + let(:project) { create(:project) } + let(:path) { namespace_project_path(project.namespace, project) } - describe "DELETE /projects/:id" do before do - @project = create(:project, namespace: @user.namespace) - @project.team << [@user, :master] - visit edit_namespace_project_path(@project.namespace, @project) + login_as(:admin) end - it "should remove project" do - expect { remove_project }.to change {Project.count}.by(-1) + it 'parses Markdown' do + project.update_attribute(:description, 'This is **my** project') + visit path + expect(page).to have_css('.project-home-desc > p > strong') + end + + it 'passes through html-pipeline' do + project.update_attribute(:description, 'This project is the :poop:') + visit path + expect(page).to have_css('.project-home-desc > p > img') + end + + it 'sanitizes unwanted tags' do + project.update_attribute(:description, '# Project Description') + visit path + expect(page).not_to have_css('.project-home-desc h1') + end + + it 'permits `rel` attribute on links' do + project.update_attribute(:description, 'https://google.com/') + visit path + expect(page).to have_css('.project-home-desc a[rel]') end + end + + describe 'removal', js: true do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } - it 'should delete the project from disk' do - expect(GitlabShellWorker).to( - receive(:perform_async).with(:remove_repository, - /#{@project.path_with_namespace}/) - ).twice + before do + login_with(user) + project.team << [user, :master] + visit edit_namespace_project_path(project.namespace, project) + end - remove_project + it 'should remove project' do + expect { remove_project }.to change {Project.count}.by(-1) end end def remove_project click_link "Remove project" - fill_in 'confirm_name_input', with: @project.path + fill_in 'confirm_name_input', with: project.path click_button 'Confirm' end end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index d0b200a9ff8..bbb434638ce 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -94,6 +94,12 @@ describe GitlabMarkdownHelper do expect(link_to_gfm(actual, commit_path)). to match('<h1>test</h1>') end + + it 'ignores reference links when they are the entire body' do + text = issues[0].to_reference + act = link_to_gfm(text, '/foo') + expect(act).to eq %Q(<a href="/foo">#{issues[0].to_reference}</a>) + end end describe '#render_wiki_content' do diff --git a/spec/javascripts/extensions/array_spec.js.coffee b/spec/javascripts/extensions/array_spec.js.coffee new file mode 100644 index 00000000000..4ceac619422 --- /dev/null +++ b/spec/javascripts/extensions/array_spec.js.coffee @@ -0,0 +1,12 @@ +#= require extensions/array + +describe 'Array extensions', -> + describe 'first', -> + it 'returns the first item', -> + arr = [0, 1, 2, 3, 4, 5] + expect(arr.first()).toBe(0) + + describe 'last', -> + it 'returns the last item', -> + arr = [0, 1, 2, 3, 4, 5] + expect(arr.last()).toBe(5) diff --git a/spec/javascripts/extensions/jquery_spec.js.coffee b/spec/javascripts/extensions/jquery_spec.js.coffee new file mode 100644 index 00000000000..b10e16b7d01 --- /dev/null +++ b/spec/javascripts/extensions/jquery_spec.js.coffee @@ -0,0 +1,34 @@ +#= require extensions/jquery + +describe 'jQuery extensions', -> + describe 'disable', -> + beforeEach -> + fixture.set '<input type="text" />' + + it 'adds the disabled attribute', -> + $input = $('input').first() + + $input.disable() + expect($input).toHaveAttr('disabled', 'disabled') + + it 'adds the disabled class', -> + $input = $('input').first() + + $input.disable() + expect($input).toHaveClass('disabled') + + describe 'enable', -> + beforeEach -> + fixture.set '<input type="text" disabled="disabled" class="disabled" />' + + it 'removes the disabled attribute', -> + $input = $('input').first() + + $input.enable() + expect($input).not.toHaveAttr('disabled') + + it 'removes the disabled class', -> + $input = $('input').first() + + $input.enable() + expect($input).not.toHaveClass('disabled') diff --git a/spec/javascripts/fixtures/issuable.html.haml b/spec/javascripts/fixtures/issuable.html.haml new file mode 100644 index 00000000000..42ab4aa68b1 --- /dev/null +++ b/spec/javascripts/fixtures/issuable.html.haml @@ -0,0 +1,2 @@ +%form.js-main-target-form + %textarea#note_note diff --git a/spec/javascripts/fixtures/issue_note.html.haml b/spec/javascripts/fixtures/issue_note.html.haml new file mode 100644 index 00000000000..0aecc7334fd --- /dev/null +++ b/spec/javascripts/fixtures/issue_note.html.haml @@ -0,0 +1,12 @@ +%ul + %li.note + .js-task-list-container + .note-text + %ul.task-list + %li.task-list-item + %input.task-list-item-checkbox{type: 'checkbox'} + Task List Item + .note-edit-form + %form + %textarea.js-task-list-field + \- [ ] Task List Item diff --git a/spec/javascripts/fixtures/issues_show.html.haml b/spec/javascripts/fixtures/issues_show.html.haml new file mode 100644 index 00000000000..db5abe0cae3 --- /dev/null +++ b/spec/javascripts/fixtures/issues_show.html.haml @@ -0,0 +1,13 @@ +%a.btn-close + +.issue-details + .description.js-task-list-container + .wiki + %ul.task-list + %li.task-list-item + %input.task-list-item-checkbox{type: 'checkbox'} + Task List Item + %textarea.js-task-list-field + \- [ ] Task List Item + +%form.js-issue-update{action: '/foo'} diff --git a/spec/javascripts/fixtures/merge_requests_show.html.haml b/spec/javascripts/fixtures/merge_requests_show.html.haml new file mode 100644 index 00000000000..c4329b8f94a --- /dev/null +++ b/spec/javascripts/fixtures/merge_requests_show.html.haml @@ -0,0 +1,13 @@ +%a.btn-close + +.merge-request-details + .description.js-task-list-container + .wiki + %ul.task-list + %li.task-list-item + %input.task-list-item-checkbox{type: 'checkbox'} + Task List Item + %textarea.js-task-list-field + \- [ ] Task List Item + +%form.js-merge-request-update{action: '/foo'} diff --git a/spec/javascripts/fixtures/zen_mode.html.haml b/spec/javascripts/fixtures/zen_mode.html.haml new file mode 100644 index 00000000000..e867e4de2b9 --- /dev/null +++ b/spec/javascripts/fixtures/zen_mode.html.haml @@ -0,0 +1,9 @@ +.zennable + %input#zen-toggle-comment.zen-toggle-comment{ tabindex: '-1', type: 'checkbox' } + .zen-backdrop + %textarea#note_note.js-gfm-input.markdown-area{placeholder: 'Leave a comment'} + %a.zen-enter-link{tabindex: '-1'} + %i.fa.fa-expand + Edit in fullscreen + %a.zen-leave-link + %i.fa.fa-compress diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee index 13b25862f57..268e4c68c31 100644 --- a/spec/javascripts/issue_spec.js.coffee +++ b/spec/javascripts/issue_spec.js.coffee @@ -1,34 +1,20 @@ -#= require jquery -#= require jasmine-fixture #= require issue describe 'Issue', -> describe 'task lists', -> - selectors = { - container: '.issue-details .description.js-task-list-container' - item: '.wiki ul.task-list li.task-list-item input.task-list-item-checkbox[type=checkbox] {Task List Item}' - textarea: '.wiki textarea.js-task-list-field{- [ ] Task List Item}' - form: 'form.js-issue-update[action="/foo"]' - close: 'a.btn-close' - } + fixture.preload('issues_show.html') beforeEach -> - $container = affix(selectors.container) - - # # These two elements are siblings inside the container - $container.find('.js-task-list-container').append(affix(selectors.item)) - $container.find('.js-task-list-container').append(affix(selectors.textarea)) - - # Task lists don't get initialized unless this button exists. Not ideal. - $container.append(affix(selectors.close)) - - # This form is used to get the `update` URL. Not ideal. - $container.append(affix(selectors.form)) - + fixture.load('issues_show.html') @issue = new Issue() + it 'modifies the Markdown field', -> + spyOn(jQuery, 'ajax').and.stub() + $('input[type=checkbox]').attr('checked', true).trigger('change') + expect($('.js-task-list-field').val()).toBe('- [x] Task List Item') + it 'submits an ajax request on tasklist:changed', -> - spyOn($, 'ajax').and.callFake (req) -> + spyOn(jQuery, 'ajax').and.callFake (req) -> expect(req.type).toBe('PATCH') expect(req.url).toBe('/foo') expect(req.data.issue.description).not.toBe(null) diff --git a/spec/javascripts/merge_request_spec.js.coffee b/spec/javascripts/merge_request_spec.js.coffee index 3ebc4a4eed5..a4735af0343 100644 --- a/spec/javascripts/merge_request_spec.js.coffee +++ b/spec/javascripts/merge_request_spec.js.coffee @@ -1,34 +1,23 @@ -#= require jquery -#= require jasmine-fixture #= require merge_request +window.disableButtonIfEmptyField = -> null + describe 'MergeRequest', -> describe 'task lists', -> - selectors = { - container: '.merge-request-details .description.js-task-list-container' - item: '.wiki ul.task-list li.task-list-item input.task-list-item-checkbox[type=checkbox] {Task List Item}' - textarea: '.wiki textarea.js-task-list-field{- [ ] Task List Item}' - form: 'form.js-merge-request-update[action="/foo"]' - close: 'a.btn-close' - } + fixture.preload('merge_requests_show.html') beforeEach -> - $container = affix(selectors.container) - - # # These two elements are siblings inside the container - $container.find('.js-task-list-container').append(affix(selectors.item)) - $container.find('.js-task-list-container').append(affix(selectors.textarea)) - - # Task lists don't get initialized unless this button exists. Not ideal. - $container.append(affix(selectors.close)) + fixture.load('merge_requests_show.html') + @merge = new MergeRequest({}) - # This form is used to get the `update` URL. Not ideal. - $container.append(affix(selectors.form)) + it 'modifies the Markdown field', -> + spyOn(jQuery, 'ajax').and.stub() - @merge = new MergeRequest({}) + $('input[type=checkbox]').attr('checked', true).trigger('change') + expect($('.js-task-list-field').val()).toBe('- [x] Task List Item') it 'submits an ajax request on tasklist:changed', -> - spyOn($, 'ajax').and.callFake (req) -> + spyOn(jQuery, 'ajax').and.callFake (req) -> expect(req.type).toBe('PATCH') expect(req.url).toBe('/foo') expect(req.data.merge_request.description).not.toBe(null) diff --git a/spec/javascripts/notes_spec.js.coffee b/spec/javascripts/notes_spec.js.coffee index de2e8e7f6c8..050b6e362c6 100644 --- a/spec/javascripts/notes_spec.js.coffee +++ b/spec/javascripts/notes_spec.js.coffee @@ -1,5 +1,3 @@ -#= require jquery -#= require jasmine-fixture #= require notes window.gon = {} @@ -7,21 +5,18 @@ window.disableButtonIfEmptyField = -> null describe 'Notes', -> describe 'task lists', -> - selectors = { - container: 'li.note .js-task-list-container' - item: '.note-text ul.task-list li.task-list-item input.task-list-item-checkbox[type=checkbox] {Task List Item}' - textarea: '.note-edit-form form textarea.js-task-list-field{- [ ] Task List Item}' - } + fixture.preload('issue_note.html') beforeEach -> - $container = affix(selectors.container) - - # These two elements are siblings inside the container - $container.find('.js-task-list-container').append(affix(selectors.item)) - $container.find('.js-task-list-container').append(affix(selectors.textarea)) + fixture.load('issue_note.html') + $('form').on 'submit', (e) -> e.preventDefault() @notes = new Notes() + it 'modifies the Markdown field', -> + $('input[type=checkbox]').attr('checked', true).trigger('change') + expect($('.js-task-list-field').val()).toBe('- [x] Task List Item') + it 'submits the form on tasklist:changed', -> submitted = false $('form').on 'submit', (e) -> submitted = true; e.preventDefault() diff --git a/spec/javascripts/shortcuts_issuable_spec.js.coffee b/spec/javascripts/shortcuts_issuable_spec.js.coffee index 57dcc2161d3..a01ad7140dd 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js.coffee +++ b/spec/javascripts/shortcuts_issuable_spec.js.coffee @@ -1,10 +1,10 @@ -#= require jquery -#= require jasmine-fixture - #= require shortcuts_issuable describe 'ShortcutsIssuable', -> + fixture.preload('issuable.html') + beforeEach -> + fixture.load('issuable.html') @shortcut = new ShortcutsIssuable() describe '#replyWithSelectedText', -> @@ -14,7 +14,6 @@ describe 'ShortcutsIssuable', -> beforeEach -> @selector = 'form.js-main-target-form textarea#note_note' - affix(@selector) describe 'with empty selection', -> it 'does nothing', -> diff --git a/spec/javascripts/spec_helper.coffee b/spec/javascripts/spec_helper.coffee new file mode 100644 index 00000000000..47b41dd2c81 --- /dev/null +++ b/spec/javascripts/spec_helper.coffee @@ -0,0 +1,46 @@ +# PhantomJS (Teaspoons default driver) doesn't have support for +# Function.prototype.bind, which has caused confusion. Use this polyfill to +# avoid the confusion. + +#= require support/bind-poly + +# You can require your own javascript files here. By default this will include +# everything in application, however you may get better load performance if you +# require the specific files that are being used in the spec that tests them. + +#= require jquery +#= require bootstrap +#= require underscore + +# Teaspoon includes some support files, but you can use anything from your own +# support path too. + +# require support/jasmine-jquery-1.7.0 +# require support/jasmine-jquery-2.0.0 +#= require support/jasmine-jquery-2.1.0 +# require support/sinon +# require support/your-support-file + +# Deferring execution + +# If you're using CommonJS, RequireJS or some other asynchronous library you can +# defer execution. Call Teaspoon.execute() after everything has been loaded. +# Simple example of a timeout: + +# Teaspoon.defer = true +# setTimeout(Teaspoon.execute, 1000) + +# Matching files + +# By default Teaspoon will look for files that match +# _spec.{js,js.coffee,.coffee}. Add a filename_spec.js file in your spec path +# and it'll be included in the default suite automatically. If you want to +# customize suites, check out the configuration in teaspoon_env.rb + +# Manifest + +# If you'd rather require your spec files manually (to control order for +# instance) you can disable the suite matcher in the configuration and use this +# file as a manifest. + +# For more information: http://github.com/modeset/teaspoon diff --git a/spec/javascripts/support/jasmine.yml b/spec/javascripts/support/jasmine.yml deleted file mode 100644 index 168c9618643..00000000000 --- a/spec/javascripts/support/jasmine.yml +++ /dev/null @@ -1,15 +0,0 @@ -# path to parent directory of spec_files -# relative path from Rails.root -# -# Alternatively accept an array of directory to include external spec files -# spec_dir: -# - spec/javascripts -# - ../engine/spec/javascripts -# -# defaults to spec/javascripts -spec_dir: spec/javascripts - -# list of file expressions to include as specs into spec runner -# relative path from spec_dir -spec_files: - - "**/*[Ss]pec.{js.coffee,js,coffee}" diff --git a/spec/javascripts/support/jasmine_helper.rb b/spec/javascripts/support/jasmine_helper.rb deleted file mode 100644 index 4d73aec5a31..00000000000 --- a/spec/javascripts/support/jasmine_helper.rb +++ /dev/null @@ -1,15 +0,0 @@ -#Use this file to set/override Jasmine configuration options -#You can remove it if you don't need it. -#This file is loaded *after* jasmine.yml is interpreted. -# -#Example: using a different boot file. -#Jasmine.configure do |config| -# config.boot_dir = '/absolute/path/to/boot_dir' -# config.boot_files = lambda { ['/absolute/path/to/boot_dir/file.js'] } -#end -# -#Example: prevent PhantomJS auto install, uses PhantomJS already on your path. -#Jasmine.configure do |config| -# config.prevent_phantom_js_auto_install = true -#end -# diff --git a/spec/javascripts/zen_mode_spec.js.coffee b/spec/javascripts/zen_mode_spec.js.coffee new file mode 100644 index 00000000000..1f4ea58ad48 --- /dev/null +++ b/spec/javascripts/zen_mode_spec.js.coffee @@ -0,0 +1,52 @@ +#= require zen_mode + +describe 'ZenMode', -> + fixture.preload('zen_mode.html') + + beforeEach -> + fixture.load('zen_mode.html') + + # Stub Dropzone.forElement(...).enable() + spyOn(Dropzone, 'forElement').and.callFake -> + enable: -> true + + @zen = new ZenMode() + + # Set this manually because we can't actually scroll the window + @zen.scroll_position = 456 + + # Ohmmmmmmm + enterZen = -> + $('.zen-toggle-comment').prop('checked', true).trigger('change') + + # Wh- what was that?! + exitZen = -> + $('.zen-toggle-comment').prop('checked', false).trigger('change') + + describe 'on enter', -> + it 'pauses Mousetrap', -> + spyOn(Mousetrap, 'pause') + enterZen() + expect(Mousetrap.pause).toHaveBeenCalled() + + describe 'in use', -> + beforeEach -> + enterZen() + + it 'exits on Escape', -> + $(document).trigger(jQuery.Event('keydown', {keyCode: 27})) + expect($('.zen-toggle-comment').prop('checked')).toBe(false) + + describe 'on exit', -> + beforeEach -> + enterZen() + + it 'unpauses Mousetrap', -> + spyOn(Mousetrap, 'unpause') + exitZen() + expect(Mousetrap.unpause).toHaveBeenCalled() + + it 'restores the scroll position', -> + spyOn(@zen, 'restoreScroll') + exitZen() + expect(@zen.restoreScroll).toHaveBeenCalledWith(456) diff --git a/spec/lib/gitlab/markdown/autolink_filter_spec.rb b/spec/lib/gitlab/markdown/autolink_filter_spec.rb index 0bbdc11a979..a14cb2da089 100644 --- a/spec/lib/gitlab/markdown/autolink_filter_spec.rb +++ b/spec/lib/gitlab/markdown/autolink_filter_spec.rb @@ -2,11 +2,9 @@ require 'spec_helper' module Gitlab::Markdown describe AutolinkFilter do - let(:link) { 'http://about.gitlab.com/' } + include FilterSpecHelper - def filter(html, options = {}) - described_class.call(html, options) - end + let(:link) { 'http://about.gitlab.com/' } it 'does nothing when :autolink is false' do exp = act = link diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb index d3695ee46d0..e8391cc7aca 100644 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' module Gitlab::Markdown describe CommitRangeReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper let(:project) { create(:project) } let(:commit1) { project.commit } diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb index a0d2cd7e22b..a10d43c9a02 100644 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' module Gitlab::Markdown describe CommitReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper let(:project) { create(:project) } let(:commit) { project.commit } diff --git a/spec/lib/gitlab/markdown/emoji_filter_spec.rb b/spec/lib/gitlab/markdown/emoji_filter_spec.rb index 18d55c4818f..11efd9bb4cd 100644 --- a/spec/lib/gitlab/markdown/emoji_filter_spec.rb +++ b/spec/lib/gitlab/markdown/emoji_filter_spec.rb @@ -2,9 +2,7 @@ require 'spec_helper' module Gitlab::Markdown describe EmojiFilter do - def filter(html, contexts = {}) - described_class.call(html, contexts) - end + include FilterSpecHelper before do ActionController::Base.asset_host = 'https://foo.com' diff --git a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb index bf9409589fa..f16095bc2b2 100644 --- a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' module Gitlab::Markdown describe ExternalIssueReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper def helper IssuesHelper diff --git a/spec/lib/gitlab/markdown/external_link_filter_spec.rb b/spec/lib/gitlab/markdown/external_link_filter_spec.rb index c2ff4f80a42..a040b34577b 100644 --- a/spec/lib/gitlab/markdown/external_link_filter_spec.rb +++ b/spec/lib/gitlab/markdown/external_link_filter_spec.rb @@ -2,9 +2,7 @@ require 'spec_helper' module Gitlab::Markdown describe ExternalLinkFilter do - def filter(html, options = {}) - described_class.call(html, options) - end + include FilterSpecHelper it 'ignores elements without an href attribute' do exp = act = %q(<a id="ignored">Ignore Me</a>) diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb index a838d7570c8..fa43d33794d 100644 --- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' module Gitlab::Markdown describe IssueReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper def helper IssuesHelper diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb index 41987f57bca..cf3337b1ba1 100644 --- a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb @@ -3,7 +3,7 @@ require 'html/pipeline' module Gitlab::Markdown describe LabelReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper let(:project) { create(:empty_project) } let(:label) { create(:label, project: project) } diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb index 6aeb1093602..5945302a2da 100644 --- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' module Gitlab::Markdown describe MergeRequestReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper let(:project) { create(:project) } let(:merge) { create(:merge_request, source_project: project) } diff --git a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb index 4a1aa766149..e50c82d0b3c 100644 --- a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb +++ b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb @@ -2,9 +2,7 @@ require 'spec_helper' module Gitlab::Markdown describe SanitizationFilter do - def filter(html, options = {}) - described_class.call(html, options) - end + include FilterSpecHelper describe 'default whitelist' do it 'sanitizes tags that are not whitelisted' do @@ -42,6 +40,13 @@ module Gitlab::Markdown end describe 'custom whitelist' do + it 'customizes the whitelist only once' do + instance = described_class.new('Foo') + 3.times { instance.whitelist } + + expect(instance.whitelist[:transformers].size).to eq 4 + end + it 'allows syntax highlighting' do exp = act = %q{<pre class="code highlight white c"><code><span class="k">def</span></code></pre>} expect(filter(act).to_html).to eq exp @@ -87,5 +92,27 @@ module Gitlab::Markdown expect(doc.at_css('a')['href']).to be_nil end end + + context 'when pipeline is :description' do + it 'uses a stricter whitelist' do + doc = filter('<h1>Description</h1>', pipeline: :description) + expect(doc.to_html.strip).to eq 'Description' + end + + %w(pre code img ol ul li).each do |elem| + it "removes '#{elem}' elements" do + act = "<#{elem}>Description</#{elem}>" + expect(filter(act, pipeline: :description).to_html.strip). + to eq 'Description' + end + end + + %w(b i strong em a ins del sup sub p).each do |elem| + it "still allows '#{elem}' elements" do + exp = act = "<#{elem}>Description</#{elem}>" + expect(filter(act, pipeline: :description).to_html).to eq exp + end + end + end end end diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb index 07ece66e903..38619a3c07f 100644 --- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' module Gitlab::Markdown describe SnippetReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper let(:project) { create(:empty_project) } let(:snippet) { create(:project_snippet, project: project) } diff --git a/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb b/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb index f383a5850d5..ddf583a72c1 100644 --- a/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb +++ b/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb @@ -4,9 +4,7 @@ require 'spec_helper' module Gitlab::Markdown describe TableOfContentsFilter do - def filter(html, options = {}) - described_class.call(html, options) - end + include FilterSpecHelper def header(level, text) "<h#{level}>#{text}</h#{level}>\n" diff --git a/spec/lib/gitlab/markdown/task_list_filter_spec.rb b/spec/lib/gitlab/markdown/task_list_filter_spec.rb index 2a1e1cc5127..94f39cc966e 100644 --- a/spec/lib/gitlab/markdown/task_list_filter_spec.rb +++ b/spec/lib/gitlab/markdown/task_list_filter_spec.rb @@ -2,9 +2,7 @@ require 'spec_helper' module Gitlab::Markdown describe TaskListFilter do - def filter(html, options = {}) - described_class.call(html, options) - end + include FilterSpecHelper it 'does not apply `task-list` class to non-task lists' do exp = act = %(<ul><li>Item</li></ul>) diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb index 0ecbdee9b9e..08e6941028f 100644 --- a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' module Gitlab::Markdown describe UserReferenceFilter do - include ReferenceFilterSpecHelper + include FilterSpecHelper let(:project) { create(:empty_project) } let(:user) { create(:user) } diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 44cdd1e4fab..2a982e8b107 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -13,6 +13,7 @@ describe Gitlab::OAuth::User do email: 'john@mail.com' } end + let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') } describe :persisted? do let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } @@ -32,31 +33,94 @@ describe Gitlab::OAuth::User do let(:provider) { 'twitter' } describe 'signup' do - context "with allow_single_sign_on enabled" do - before { Gitlab.config.omniauth.stub allow_single_sign_on: true } + shared_examples "to verify compliance with allow_single_sign_on" do + context "with allow_single_sign_on enabled" do + before { Gitlab.config.omniauth.stub allow_single_sign_on: true } - it "creates a user from Omniauth" do - oauth_user.save + it "creates a user from Omniauth" do + oauth_user.save - expect(gl_user).to be_valid - identity = gl_user.identities.first - expect(identity.extern_uid).to eql uid - expect(identity.provider).to eql 'twitter' + expect(gl_user).to be_valid + identity = gl_user.identities.first + expect(identity.extern_uid).to eql uid + expect(identity.provider).to eql 'twitter' + end + end + + context "with allow_single_sign_on disabled (Default)" do + before { Gitlab.config.omniauth.stub allow_single_sign_on: false } + it "throws an error" do + expect{ oauth_user.save }.to raise_error StandardError + end end end - context "with allow_single_sign_on disabled (Default)" do - it "throws an error" do - expect{ oauth_user.save }.to raise_error StandardError + context "with auto_link_ldap_user disabled (default)" do + before { Gitlab.config.omniauth.stub auto_link_ldap_user: false } + include_examples "to verify compliance with allow_single_sign_on" + end + + context "with auto_link_ldap_user enabled" do + before { Gitlab.config.omniauth.stub auto_link_ldap_user: true } + + context "and a corresponding LDAP person" do + before do + ldap_user.stub(:uid) { uid } + ldap_user.stub(:username) { uid } + ldap_user.stub(:email) { ['johndoe@example.com','john2@example.com'] } + ldap_user.stub(:dn) { 'uid=user1,ou=People,dc=example' } + allow(oauth_user).to receive(:ldap_person).and_return(ldap_user) + end + + context "and no account for the LDAP user" do + + it "creates a user with dual LDAP and omniauth identities" do + oauth_user.save + + expect(gl_user).to be_valid + expect(gl_user.username).to eql uid + expect(gl_user.email).to eql 'johndoe@example.com' + expect(gl_user.identities.length).to eql 2 + identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } + expect(identities_as_hash).to match_array( + [ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + { provider: 'twitter', extern_uid: uid } + ]) + end + end + + context "and LDAP user has an account already" do + let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } + it "adds the omniauth identity to the LDAP account" do + oauth_user.save + + expect(gl_user).to be_valid + expect(gl_user.username).to eql 'john' + expect(gl_user.email).to eql 'john@example.com' + expect(gl_user.identities.length).to eql 2 + identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } + expect(identities_as_hash).to match_array( + [ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + { provider: 'twitter', extern_uid: uid } + ]) + end + end + end + + context "and no corresponding LDAP person" do + before { allow(oauth_user).to receive(:ldap_person).and_return(nil) } + + include_examples "to verify compliance with allow_single_sign_on" end end + end describe 'blocking' do let(:provider) { 'twitter' } before { Gitlab.config.omniauth.stub allow_single_sign_on: true } - context 'signup' do + context 'signup with omniauth only' do context 'dont block on create' do before { Gitlab.config.omniauth.stub block_auto_created_users: false } @@ -78,6 +142,64 @@ describe Gitlab::OAuth::User do end end + context 'signup with linked omniauth and LDAP account' do + before do + Gitlab.config.omniauth.stub auto_link_ldap_user: true + ldap_user.stub(:uid) { uid } + ldap_user.stub(:username) { uid } + ldap_user.stub(:email) { ['johndoe@example.com','john2@example.com'] } + ldap_user.stub(:dn) { 'uid=user1,ou=People,dc=example' } + allow(oauth_user).to receive(:ldap_person).and_return(ldap_user) + end + + context "and no account for the LDAP user" do + context 'dont block on create (LDAP)' do + before { Gitlab::LDAP::Config.any_instance.stub block_auto_created_users: false } + + it do + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create (LDAP)' do + before { Gitlab::LDAP::Config.any_instance.stub block_auto_created_users: true } + + it do + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user).to be_blocked + end + end + end + + context 'and LDAP user has an account already' do + let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } + + context 'dont block on create (LDAP)' do + before { Gitlab::LDAP::Config.any_instance.stub block_auto_created_users: false } + + it do + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create (LDAP)' do + before { Gitlab::LDAP::Config.any_instance.stub block_auto_created_users: true } + + it do + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + end + end + + context 'sign-in' do before do oauth_user.save @@ -103,6 +225,26 @@ describe Gitlab::OAuth::User do expect(gl_user).not_to be_blocked end end + + context 'dont block on create (LDAP)' do + before { Gitlab::LDAP::Config.any_instance.stub block_auto_created_users: false } + + it do + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create (LDAP)' do + before { Gitlab::LDAP::Config.any_instance.stub block_auto_created_users: true } + + it do + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end end end end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index c14f4ac6bf6..f921dd9cc09 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -16,6 +16,30 @@ describe Gitlab::ReferenceExtractor do expect(subject.users).to eq([@u_foo, @u_bar, @u_offteam]) end + it 'ignores user mentions inside specific elements' do + @u_foo = create(:user, username: 'foo') + @u_bar = create(:user, username: 'bar') + @u_offteam = create(:user, username: 'offteam') + + project.team << [@u_foo, :reporter] + project.team << [@u_bar, :guest] + + subject.analyze(%Q{ + Inline code: `@foo` + + Code block: + + ``` + @bar + ``` + + Quote: + + > @offteam + }) + expect(subject.users).to eq([]) + end + it 'accesses valid issue objects' do @i0 = create(:issue, project: project) @i1 = create(:issue, project: project) diff --git a/spec/lib/gitlab/upgrader_spec.rb b/spec/lib/gitlab/upgrader_spec.rb index ce3ea6c260a..baa4bd0f28f 100644 --- a/spec/lib/gitlab/upgrader_spec.rb +++ b/spec/lib/gitlab/upgrader_spec.rb @@ -20,5 +20,20 @@ describe Gitlab::Upgrader do upgrader.stub(current_version_raw: "5.3.0") expect(upgrader.latest_version_raw).to eq("v5.4.2") end + + it 'should get the latest version from tags' do + allow(upgrader).to receive(:fetch_git_tags).and_return([ + '6f0733310546402c15d3ae6128a95052f6c8ea96 refs/tags/v7.1.1', + 'facfec4b242ce151af224e20715d58e628aa5e74 refs/tags/v7.1.1^{}', + 'f7068d99c79cf79befbd388030c051bb4b5e86d4 refs/tags/v7.10.4', + '337225a4fcfa9674e2528cb6d41c46556bba9dfa refs/tags/v7.10.4^{}', + '880e0ba0adbed95d087f61a9a17515e518fc6440 refs/tags/v7.11.1', + '6584346b604f981f00af8011cd95472b2776d912 refs/tags/v7.11.1^{}', + '43af3e65a486a9237f29f56d96c3b3da59c24ae0 refs/tags/v7.11.2', + 'dac18e7728013a77410e926a1e64225703754a2d refs/tags/v7.11.2^{}', + '0bf21fd4b46c980c26fd8c90a14b86a4d90cc950 refs/tags/v7.9.4', + 'b10de29edbaff7219547dc506cb1468ee35065c3 refs/tags/v7.9.4^{}']) + expect(upgrader.latest_version_raw).to eq("v7.11.2") + end end end diff --git a/spec/lib/repository_cache_spec.rb b/spec/lib/repository_cache_spec.rb index af399f3a731..37240d51310 100644 --- a/spec/lib/repository_cache_spec.rb +++ b/spec/lib/repository_cache_spec.rb @@ -1,4 +1,3 @@ -require 'rspec' require_relative '../../lib/repository_cache' describe RepositoryCache do diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 557c71b4d2c..86c395a8e8e 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -16,7 +16,7 @@ describe Issue, "Issuable" do it { is_expected.to validate_presence_of(:iid) } it { is_expected.to validate_presence_of(:author) } it { is_expected.to validate_presence_of(:title) } - it { is_expected.to ensure_length_of(:title).is_at_least(0).is_at_most(255) } + it { is_expected.to validate_length_of(:title).is_at_least(0).is_at_most(255) } end describe "Scope" do diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb index eadb941a3fa..22237f2e9f2 100644 --- a/spec/models/concerns/mentionable_spec.rb +++ b/spec/models/concerns/mentionable_spec.rb @@ -1,14 +1,31 @@ require 'spec_helper' describe Issue, "Mentionable" do - describe :mentioned_users do + describe '#mentioned_users' do let!(:user) { create(:user, username: 'stranger') } let!(:user2) { create(:user, username: 'john') } - let!(:issue) { create(:issue, description: '@stranger mentioned') } + let!(:issue) { create(:issue, description: "#{user.to_reference} mentioned") } subject { issue.mentioned_users } it { is_expected.to include(user) } it { is_expected.not_to include(user2) } end + + describe '#create_cross_references!' do + let(:project) { create(:project) } + let(:author) { double('author') } + let(:commit) { project.commit } + let(:commit2) { project.commit } + + let!(:issue) do + create(:issue, project: project, description: commit.to_reference) + end + + it 'correctly removes already-mentioned Commits' do + expect(Note).not_to receive(:create_cross_reference_note) + + issue.create_cross_references!(project, author, [commit2]) + end + end end diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb index 7032b777144..705ef257d86 100644 --- a/spec/models/deploy_keys_project_spec.rb +++ b/spec/models/deploy_keys_project_spec.rb @@ -36,9 +36,7 @@ describe DeployKeysProject do it "doesn't destroy the deploy key" do subject.destroy - expect { - deploy_key.reload - }.not_to raise_error(ActiveRecord::RecordNotFound) + expect { deploy_key.reload }.not_to raise_error end end @@ -63,9 +61,7 @@ describe DeployKeysProject do it "doesn't destroy the deploy key" do subject.destroy - expect { - deploy_key.reload - }.not_to raise_error(ActiveRecord::RecordNotFound) + expect { deploy_key.reload }.not_to raise_error end end end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 6eb1208a7f2..fbb9e162952 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -26,8 +26,8 @@ describe Key do describe "Validation" do it { is_expected.to validate_presence_of(:title) } it { is_expected.to validate_presence_of(:key) } - it { is_expected.to ensure_length_of(:title).is_within(0..255) } - it { is_expected.to ensure_length_of(:key).is_within(0..5000) } + it { is_expected.to validate_length_of(:title).is_within(0..255) } + it { is_expected.to validate_length_of(:key).is_within(0..5000) } end describe "Methods" do diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb index e5bf9125313..ebd8b545aa7 100644 --- a/spec/models/project_services/gitlab_ci_service_spec.rb +++ b/spec/models/project_services/gitlab_ci_service_spec.rb @@ -48,6 +48,21 @@ describe GitlabCiService 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")} end + + describe "execute" do + let(:user) { create(:user, username: 'username') } + let(:project) { create(:project, name: 'project') } + let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } + + it "calls ci_yaml_file" do + service_hook = double + service_hook.should_receive(:execute) + @service.should_receive(:service_hook).and_return(service_hook) + @service.should_receive(:ci_yaml_file).with(push_sample_data) + + @service.execute(push_sample_data) + end + end end describe "Fork registration" do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 48568e2a3ff..87c67fa32c3 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -69,14 +69,14 @@ describe Project do it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) } - it { is_expected.to ensure_length_of(:name).is_within(0..255) } + it { is_expected.to validate_length_of(:name).is_within(0..255) } it { is_expected.to validate_presence_of(:path) } it { is_expected.to validate_uniqueness_of(:path).scoped_to(:namespace_id) } - it { is_expected.to ensure_length_of(:path).is_within(0..255) } - it { is_expected.to ensure_length_of(:description).is_within(0..2000) } + it { is_expected.to validate_length_of(:path).is_within(0..255) } + it { is_expected.to validate_length_of(:description).is_within(0..2000) } it { is_expected.to validate_presence_of(:creator) } - it { is_expected.to ensure_length_of(:issues_tracker_id).is_within(0..255) } + it { is_expected.to validate_length_of(:issues_tracker_id).is_within(0..255) } it { is_expected.to validate_presence_of(:namespace) } it 'should not allow new projects beyond user limits' do diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index c81dd36ef4b..c786d0bf103 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -38,10 +38,10 @@ describe Snippet do it { is_expected.to validate_presence_of(:author) } it { is_expected.to validate_presence_of(:title) } - it { is_expected.to ensure_length_of(:title).is_within(0..255) } + it { is_expected.to validate_length_of(:title).is_within(0..255) } it { is_expected.to validate_presence_of(:file_name) } - it { is_expected.to ensure_length_of(:file_name).is_within(0..255) } + it { is_expected.to validate_length_of(:file_name).is_within(0..255) } it { is_expected.to validate_presence_of(:content) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 49c7b7d99ce..f1b8afa5854 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -96,7 +96,7 @@ describe User do it { is_expected.to allow_value(0).for(:projects_limit) } it { is_expected.not_to allow_value(-1).for(:projects_limit) } - it { is_expected.to ensure_length_of(:bio).is_within(0..255) } + it { is_expected.to validate_length_of(:bio).is_within(0..255) } describe 'email' do it 'accepts info@example.com' do @@ -248,6 +248,7 @@ describe User do it { expect(@user.several_namespaces?).to be_truthy } it { expect(@user.authorized_groups).to eq([@group]) } it { expect(@user.owned_groups).to eq([@group]) } + it { expect(@user.namespaces).to match_array([@user.namespace, @group]) } end describe 'group multiple owners' do @@ -270,6 +271,7 @@ describe User do end it { expect(@user.several_namespaces?).to be_falsey } + it { expect(@user.namespaces).to eq([@user.namespace]) } end describe 'blocking user' do diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 9ea60e1a4ad..a1c248c636e 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -9,6 +9,7 @@ describe API::API, api: true do let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } + let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') } before { project.team << [user, :reporter] } @@ -89,7 +90,7 @@ describe API::API, api: true do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user) expect(response.status).to eq(200) expect(json_response).to be_an Array - expect(json_response.length).to eq(1) + expect(json_response.length).to eq(2) expect(json_response.first['note']).to eq('a comment on a commit') expect(json_response.first['author']['id']).to eq(user.id) end diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index bab8888a631..15f547e128d 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -49,10 +49,6 @@ describe API::API, api: true do } it "should create a new file in project repo" do - Gitlab::Satellite::NewFileAction.any_instance.stub( - commit!: true, - ) - post api("/projects/#{project.id}/repository/files", user), valid_params expect(response.status).to eq(201) expect(json_response['file_path']).to eq('newfile.rb') @@ -63,9 +59,9 @@ describe API::API, api: true do expect(response.status).to eq(400) end - it "should return a 400 if satellite fails to create file" do - Gitlab::Satellite::NewFileAction.any_instance.stub( - commit!: false, + it "should return a 400 if editor fails to create file" do + Repository.any_instance.stub( + commit_file: false, ) post api("/projects/#{project.id}/repository/files", user), valid_params @@ -84,10 +80,6 @@ describe API::API, api: true do } it "should update existing file in project repo" do - Gitlab::Satellite::EditFileAction.any_instance.stub( - commit!: true, - ) - put api("/projects/#{project.id}/repository/files", user), valid_params expect(response.status).to eq(200) expect(json_response['file_path']).to eq(file_path) @@ -97,35 +89,6 @@ describe API::API, api: true do put api("/projects/#{project.id}/repository/files", user) expect(response.status).to eq(400) end - - it 'should return a 400 if the checkout fails' do - Gitlab::Satellite::EditFileAction.any_instance.stub(:commit!) - .and_raise(Gitlab::Satellite::CheckoutFailed) - - put api("/projects/#{project.id}/repository/files", user), valid_params - expect(response.status).to eq(400) - - ref = valid_params[:branch_name] - expect(response.body).to match("ref '#{ref}' could not be checked out") - end - - it 'should return a 409 if the file was not modified' do - Gitlab::Satellite::EditFileAction.any_instance.stub(:commit!) - .and_raise(Gitlab::Satellite::CommitFailed) - - put api("/projects/#{project.id}/repository/files", user), valid_params - expect(response.status).to eq(409) - expect(response.body).to match("Maybe there was nothing to commit?") - end - - it 'should return a 409 if the push fails' do - Gitlab::Satellite::EditFileAction.any_instance.stub(:commit!) - .and_raise(Gitlab::Satellite::PushFailed) - - put api("/projects/#{project.id}/repository/files", user), valid_params - expect(response.status).to eq(409) - expect(response.body).to match("Maybe the file was changed by another process?") - end end describe "DELETE /projects/:id/repository/files" do @@ -138,10 +101,6 @@ describe API::API, api: true do } it "should delete existing file in project repo" do - Gitlab::Satellite::DeleteFileAction.any_instance.stub( - commit!: true, - ) - delete api("/projects/#{project.id}/repository/files", user), valid_params expect(response.status).to eq(200) expect(json_response['file_path']).to eq(file_path) @@ -153,8 +112,8 @@ describe API::API, api: true do end it "should return a 400 if satellite fails to create file" do - Gitlab::Satellite::DeleteFileAction.any_instance.stub( - commit!: false, + Repository.any_instance.stub( + remove_file: false, ) delete api("/projects/#{project.id}/repository/files", user), valid_params diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index dcd50f73326..0ed5883914b 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -349,10 +349,10 @@ describe API::API, api: true do expect(json_response['description']).to eq('New description') end - it "should return 422 when source_branch and target_branch are renamed the same" do + it "should return 400 when source_branch is specified" do put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), source_branch: "master", target_branch: "master" - expect(response.status).to eq(422) + expect(response.status).to eq(400) end it "should return merge_request with renamed target_branch" do diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index 6ddaaa0a6dd..21787fdd895 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers let(:admin) { create(:admin) } + let(:user) { create(:user) } let!(:group1) { create(:group) } let!(:group2) { create(:group) } @@ -14,7 +15,7 @@ describe API::API, api: true do end end - context "when authenticated as admin" do + context "when authenticated as admin" do it "admin: should return an array of all namespaces" do get api("/namespaces", admin) expect(response.status).to eq(200) @@ -22,6 +23,32 @@ describe API::API, api: true do expect(json_response.length).to eq(Namespace.count) end + + it "admin: should return an array of matched namespaces" do + get api("/namespaces?search=#{group1.name}", admin) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + + expect(json_response.length).to eq(1) + end + end + + context "when authenticated as a regular user" do + it "user: should return an array of namespaces" do + get api("/namespaces", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + + expect(json_response.length).to eq(1) + end + + it "admin: should return an array of matched namespaces" do + get api("/namespaces?search=#{user.username}", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + + expect(json_response.length).to eq(1) + end end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 46cd26eb927..dbfd72e5f19 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -57,14 +57,14 @@ describe API::API, api: true do expect(json_response.first['name']).to eq(project.name) expect(json_response.first['owner']['username']).to eq(user.username) end - + it 'should include the project labels as the tag_list' do get api('/projects', user) response.status.should == 200 json_response.should be_an Array json_response.first.keys.should include('tag_list') end - + context 'and using search' do it 'should return searched project' do get api('/projects', user), { search: project.name } @@ -792,11 +792,6 @@ describe API::API, api: true do describe 'DELETE /projects/:id' do context 'when authenticated as user' do it 'should remove project' do - expect(GitlabShellWorker).to( - receive(:perform_async).with(:remove_repository, - /#{project.path_with_namespace}/) - ).twice - delete api("/projects/#{project.id}", user) expect(response.status).to eq(200) end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 042352311da..0040718d9be 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -172,7 +172,7 @@ end # DELETE /:project_id/deploy_keys/:id(.:format) deploy_keys#destroy describe Projects::DeployKeysController, 'routing' do it_behaves_like 'RESTful project resources' do - let(:actions) { [:index, :show, :new, :create] } + let(:actions) { [:index, :new, :create] } let(:controller) { 'deploy_keys' } end end @@ -208,23 +208,31 @@ describe Projects::RefsController, 'routing' do end end -# diffs_project_merge_request GET /:project_id/merge_requests/:id/diffs(.:format) projects/merge_requests#diffs -# automerge_project_merge_request POST /:project_id/merge_requests/:id/automerge(.:format) projects/merge_requests#automerge -# automerge_check_project_merge_request GET /:project_id/merge_requests/:id/automerge_check(.:format) projects/merge_requests#automerge_check -# branch_from_project_merge_requests GET /:project_id/merge_requests/branch_from(.:format) projects/merge_requests#branch_from -# branch_to_project_merge_requests GET /:project_id/merge_requests/branch_to(.:format) projects/merge_requests#branch_to -# project_merge_requests GET /:project_id/merge_requests(.:format) projects/merge_requests#index -# POST /:project_id/merge_requests(.:format) projects/merge_requests#create -# new_project_merge_request GET /:project_id/merge_requests/new(.:format) projects/merge_requests#new -# edit_project_merge_request GET /:project_id/merge_requests/:id/edit(.:format) projects/merge_requests#edit -# project_merge_request GET /:project_id/merge_requests/:id(.:format) projects/merge_requests#show -# PUT /:project_id/merge_requests/:id(.:format) projects/merge_requests#update -# DELETE /:project_id/merge_requests/:id(.:format) projects/merge_requests#destroy +# diffs_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/diffs(.:format) projects/merge_requests#diffs +# commits_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/commits(.:format) projects/merge_requests#commits +# automerge_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/automerge(.:format) projects/merge_requests#automerge +# automerge_check_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/automerge_check(.:format) projects/merge_requests#automerge_check +# ci_status_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/ci_status(.:format) projects/merge_requests#ci_status +# toggle_subscription_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/toggle_subscription(.:format) projects/merge_requests#toggle_subscription +# branch_from_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_from(.:format) projects/merge_requests#branch_from +# branch_to_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_to(.:format) projects/merge_requests#branch_to +# update_branches_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/update_branches(.:format) projects/merge_requests#update_branches +# namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests(.:format) projects/merge_requests#index +# POST /:namespace_id/:project_id/merge_requests(.:format) projects/merge_requests#create +# new_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/new(.:format) projects/merge_requests#new +# edit_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/edit(.:format) projects/merge_requests#edit +# namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#show +# PATCH /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#update +# PUT /:namespace_id/:project_id/merge_requests/:id(.:format) projects/merge_requests#update describe Projects::MergeRequestsController, 'routing' do it 'to #diffs' do expect(get('/gitlab/gitlabhq/merge_requests/1/diffs')).to route_to('projects/merge_requests#diffs', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end + it 'to #commits' do + expect(get('/gitlab/gitlabhq/merge_requests/1/commits')).to route_to('projects/merge_requests#commits', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + end + it 'to #automerge' do expect(post('/gitlab/gitlabhq/merge_requests/1/automerge')).to route_to( 'projects/merge_requests#automerge', diff --git a/spec/services/destroy_group_service_spec.rb b/spec/services/destroy_group_service_spec.rb new file mode 100644 index 00000000000..24e439503e7 --- /dev/null +++ b/spec/services/destroy_group_service_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe DestroyGroupService do + let!(:user) { create(:user) } + let!(:group) { create(:group) } + let!(:project) { create(:project, namespace: group) } + let!(:gitlab_shell) { Gitlab::Shell.new } + let!(:remove_path) { group.path + "+#{group.id}+deleted" } + + context 'database records' do + before do + destroy_group(group, user) + end + + it { Group.all.should_not include(group) } + it { Project.all.should_not include(project) } + end + + context 'file system' do + context 'Sidekiq inline' do + before do + # Run sidekiq immediatly to check that renamed dir will be removed + Sidekiq::Testing.inline! { destroy_group(group, user) } + end + + it { gitlab_shell.exists?(group.path).should be_falsey } + it { gitlab_shell.exists?(remove_path).should be_falsey } + end + + context 'Sidekiq fake' do + before do + # Dont run sidekiq to check if renamed repository exists + Sidekiq::Testing.fake! { destroy_group(group, user) } + end + + it { gitlab_shell.exists?(group.path).should be_falsey } + it { gitlab_shell.exists?(remove_path).should be_truthy } + end + end + + def destroy_group(group, user) + DestroyGroupService.new(group, user).execute + end +end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 0a0760056cf..c75173c1452 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -20,7 +20,8 @@ describe MergeRequests::UpdateService do description: 'Also please fix', assignee_id: user2.id, state_event: 'close', - label_ids: [label.id] + label_ids: [label.id], + target_branch: 'target' } end @@ -39,6 +40,7 @@ describe MergeRequests::UpdateService do it { expect(@merge_request).to be_closed } it { expect(@merge_request.labels.count).to eq(1) } it { expect(@merge_request.labels.first.title).to eq('Bug') } + it { expect(@merge_request.target_branch).to eq('target') } it 'should execute hooks with update action' do expect(service).to have_received(:execute_hooks). @@ -77,6 +79,13 @@ describe MergeRequests::UpdateService do expect(note).not_to be_nil expect(note.note).to eq 'Title changed from **Old title** to **New title**' end + + it 'creates system note about branch change' do + note = find_note('Target') + + expect(note).not_to be_nil + expect(note.note).to eq 'Target branch changed from `master` to `target`' + end end end end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb new file mode 100644 index 00000000000..cdf576cc0c1 --- /dev/null +++ b/spec/services/projects/destroy_service_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Projects::DestroyService do + let!(:user) { create(:user) } + let!(:project) { create(:project, namespace: user.namespace) } + let!(:path) { project.repository.path_to_repo } + let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") } + + context 'Sidekiq inline' do + before do + # Run sidekiq immediatly to check that renamed repository will be removed + Sidekiq::Testing.inline! { destroy_project(project, user, {}) } + end + + it { Project.all.should_not include(project) } + it { Dir.exists?(path).should be_falsey } + it { Dir.exists?(remove_path).should be_falsey } + end + + context 'Sidekiq fake' do + before do + # Dont run sidekiq to check if renamed repository exists + Sidekiq::Testing.fake! { destroy_project(project, user, {}) } + end + + it { Project.all.should_not include(project) } + it { Dir.exists?(path).should be_falsey } + it { Dir.exists?(remove_path).should be_truthy } + end + + def destroy_project(project, user, params) + Projects::DestroyService.new(project, user, params).execute + end +end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 0dcc94e8bd4..700286b585a 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -228,6 +228,20 @@ describe SystemNoteService do end end + describe '.change_branch' do + subject { described_class.change_branch(noteable, project, author, 'target', old_branch, new_branch) } + let(:old_branch) { 'old_branch'} + let(:new_branch) { 'new_branch'} + + it_behaves_like 'a system note' + + context 'when target branch name changed' do + it 'sets the note text' do + expect(subject.note).to eq "Target branch changed from `#{old_branch}` to `#{new_branch}`" + end + end + end + describe '.cross_reference' do subject { described_class.cross_reference(noteable, mentioner, author) } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8fe51cf4add..9c8004ab555 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,16 +1,7 @@ -if ENV['SIMPLECOV'] - require 'simplecov' -end - -if ENV['COVERALLS'] - require 'coveralls' - Coveralls.wear_merged! -end - ENV["RAILS_ENV"] ||= 'test' require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' -require 'webmock/rspec' +require 'shoulda/matchers' require 'email_spec' require 'sidekiq/testing/inline' @@ -18,8 +9,6 @@ require 'sidekiq/testing/inline' # in spec/support/ and its subdirectories. Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } -WebMock.disable_net_connect!(allow_localhost: true) - RSpec.configure do |config| config.use_transactional_fixtures = false config.use_instantiated_fixtures = false diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index ec9a326a1ea..f63322776d4 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -29,6 +29,6 @@ module ApiHelpers end def json_response - JSON.parse(response.body) + @_json_response ||= JSON.parse(response.body) end end diff --git a/spec/support/coverage.rb b/spec/support/coverage.rb new file mode 100644 index 00000000000..a54bf03380c --- /dev/null +++ b/spec/support/coverage.rb @@ -0,0 +1,8 @@ +if ENV['SIMPLECOV'] + require 'simplecov' +end + +if ENV['COVERALLS'] + require 'coveralls' + Coveralls.wear_merged! +end diff --git a/spec/support/reference_filter_spec_helper.rb b/spec/support/filter_spec_helper.rb index afbea55ab99..755964e9a3d 100644 --- a/spec/support/reference_filter_spec_helper.rb +++ b/spec/support/filter_spec_helper.rb @@ -1,46 +1,23 @@ -# Common methods and setup for Gitlab::Markdown reference filter specs +# Helper methods for Gitlab::Markdown filter specs # # Must be included into specs manually -module ReferenceFilterSpecHelper +module FilterSpecHelper extend ActiveSupport::Concern - # Shortcut to Rails' auto-generated routes helpers, to avoid including the - # module - def urls - Rails.application.routes.url_helpers - end - - # Modify a String reference to make it invalid - # - # Commit SHAs get reversed, IDs get incremented by 1, all other Strings get - # their word characters reversed. - # - # reference - String reference to modify - # - # Returns a String - def invalidate_reference(reference) - if reference =~ /\A(.+)?.\d+\z/ - # Integer-based reference with optional project prefix - reference.gsub(/\d+\z/) { |i| i.to_i + 1 } - elsif reference =~ /\A(.+@)?(\h{6,40}\z)/ - # SHA-based reference with optional prefix - reference.gsub(/\h{6,40}\z/) { |v| v.reverse } - else - reference.gsub(/\w+\z/) { |v| v.reverse } - end - end - # Perform `call` on the described class # - # Automatically passes the current `project` value to the context if none is - # provided. + # Automatically passes the current `project` value, if defined, to the context + # if none is provided. # - # html - String text to pass to the filter's `call` method. + # html - HTML String to pass to the filter's `call` method. # contexts - Hash context for the filter. (default: {project: project}) # - # Returns the String text returned by the filter's `call` method. + # Returns a Nokogiri::XML::DocumentFragment def filter(html, contexts = {}) - contexts.reverse_merge!(project: project) + if defined?(project) + contexts.reverse_merge!(project: project) + end + described_class.call(html, contexts) end @@ -50,7 +27,7 @@ module ReferenceFilterSpecHelper # body - String text to run through the pipeline # contexts - Hash context for the filter. (default: {project: project}) # - # Returns the Hash of the pipeline result + # Returns the Hash def pipeline_result(body, contexts = {}) contexts.reverse_merge!(project: project) @@ -58,13 +35,43 @@ module ReferenceFilterSpecHelper pipeline.call(body) end + # Modify a String reference to make it invalid + # + # Commit SHAs get reversed, IDs get incremented by 1, all other Strings get + # their word characters reversed. + # + # reference - String reference to modify + # + # Returns a String + def invalidate_reference(reference) + if reference =~ /\A(.+)?.\d+\z/ + # Integer-based reference with optional project prefix + reference.gsub(/\d+\z/) { |i| i.to_i + 1 } + elsif reference =~ /\A(.+@)?(\h{6,40}\z)/ + # SHA-based reference with optional prefix + reference.gsub(/\h{6,40}\z/) { |v| v.reverse } + else + reference.gsub(/\w+\z/) { |v| v.reverse } + end + end + + # Stub CrossProjectReference#user_can_reference_project? to return true for + # the current test def allow_cross_reference! allow_any_instance_of(described_class). to receive(:user_can_reference_project?).and_return(true) end + # Stub CrossProjectReference#user_can_reference_project? to return false for + # the current test def disallow_cross_reference! allow_any_instance_of(described_class). to receive(:user_can_reference_project?).and_return(false) end + + # Shortcut to Rails' auto-generated routes helpers, to avoid including the + # module + def urls + Rails.application.routes.url_helpers + end end diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb index 52b11bd6323..f8cce2ea5a3 100644 --- a/spec/support/matchers.rb +++ b/spec/support/matchers.rb @@ -70,7 +70,7 @@ end # Extend shoulda-matchers module Shoulda::Matchers::ActiveModel - class EnsureLengthOfMatcher + class ValidateLengthOfMatcher # Shortcut for is_at_least and is_at_most def is_within(range) is_at_least(range.min) && is_at_most(range.max) diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index ede62e8f37a..d29c8a55c82 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -107,17 +107,26 @@ shared_examples 'an editable mentionable' do it 'creates new cross-reference notes when the mentionable text is edited' do subject.save - new_text = <<-MSG + new_text = <<-MSG.strip_heredoc These references already existed: - Issue: #{mentioned_issue.to_reference} - Commit: #{mentioned_commit.to_reference} + + Issue: #{mentioned_issue.to_reference} + + Commit: #{mentioned_commit.to_reference} + + --- This cross-project reference already existed: - Issue: #{ext_issue.to_reference(project)} + + Issue: #{ext_issue.to_reference(project)} + + --- These two references are introduced in an edit: - Issue: #{new_issues[0].to_reference} - Cross: #{new_issues[1].to_reference(project)} + + Issue: #{new_issues[0].to_reference} + + Cross: #{new_issues[1].to_reference(project)} MSG # These three objects were already referenced, and should not receive new diff --git a/spec/support/webmock.rb b/spec/support/webmock.rb new file mode 100644 index 00000000000..af2906b7568 --- /dev/null +++ b/spec/support/webmock.rb @@ -0,0 +1,4 @@ +require 'webmock' +require 'webmock/rspec' + +WebMock.disable_net_connect!(allow_localhost: true) diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb new file mode 100644 index 00000000000..58f45ff8610 --- /dev/null +++ b/spec/teaspoon_env.rb @@ -0,0 +1,178 @@ +Teaspoon.configure do |config| + # Determines where the Teaspoon routes will be mounted. Changing this to "/jasmine" would allow you to browse to + # `http://localhost:3000/jasmine` to run your tests. + config.mount_at = "/teaspoon" + + # Specifies the root where Teaspoon will look for files. If you're testing an engine using a dummy application it can + # be useful to set this to your engines root (e.g. `Teaspoon::Engine.root`). + # Note: Defaults to `Rails.root` if nil. + config.root = nil + + # Paths that will be appended to the Rails assets paths + # Note: Relative to `config.root`. + config.asset_paths = ["spec/javascripts", "spec/javascripts/stylesheets"] + + # Fixtures are rendered through a controller, which allows using HAML, RABL/JBuilder, etc. Files in these paths will + # be rendered as fixtures. + config.fixture_paths = ["spec/javascripts/fixtures"] + + # SUITES + # + # You can modify the default suite configuration and create new suites here. Suites are isolated from one another. + # + # When defining a suite you can provide a name and a block. If the name is left blank, :default is assumed. You can + # omit various directives and the ones defined in the default suite will be used. + # + # To run a specific suite + # - in the browser: http://localhost/teaspoon/[suite_name] + # - with the rake task: rake teaspoon suite=[suite_name] + # - with the cli: teaspoon --suite=[suite_name] + config.suite do |suite| + # Specify the framework you would like to use. This allows you to select versions, and will do some basic setup for + # you -- which you can override with the directives below. This should be specified first, as it can override other + # directives. + # Note: If no version is specified, the latest is assumed. + # + # Versions: 1.3.1, 2.0.3, 2.1.3, 2.2.0 + suite.use_framework :jasmine, "2.2.0" + + # Specify a file matcher as a regular expression and all matching files will be loaded when the suite is run. These + # files need to be within an asset path. You can add asset paths using the `config.asset_paths`. + suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.coffee,coffee}" + + # Load additional JS files, but requiring them in your spec helper is the preferred way to do this. + #suite.javascripts = [] + + # You can include your own stylesheets if you want to change how Teaspoon looks. + # Note: Spec related CSS can and should be loaded using fixtures. + #suite.stylesheets = ["teaspoon"] + + # This suites spec helper, which can require additional support files. This file is loaded before any of your test + # files are loaded. + suite.helper = "spec_helper" + + # Partial to be rendered in the head tag of the runner. You can use the provided ones or define your own by creating + # a `_boot.html.erb` in your fixtures path, and adjust the config to `"/boot"` for instance. + # + # Available: boot, boot_require_js + suite.boot_partial = "boot" + + # Partial to be rendered in the body tag of the runner. You can define your own to create a custom body structure. + suite.body_partial = "body" + + # Hooks allow you to use `Teaspoon.hook("fixtures")` before, after, or during your spec run. This will make a + # synchronous Ajax request to the server that will call all of the blocks you've defined for that hook name. + #suite.hook :fixtures, &proc{} + + # Determine whether specs loaded into the test harness should be embedded as individual script tags or concatenated + # into a single file. Similar to Rails' asset `debug: true` and `config.assets.debug = true` options. By default, + # Teaspoon expands all assets to provide more valuable stack traces that reference individual source files. + #suite.expand_assets = true + end + + # Example suite. Since we're just filtering to files already within the root test/javascripts, these files will also + # be run in the default suite -- but can be focused into a more specific suite. + #config.suite :targeted do |suite| + # suite.matcher = "spec/javascripts/targeted/*_spec.{js,js.coffee,coffee}" + #end + + # CONSOLE RUNNER SPECIFIC + # + # These configuration directives are applicable only when running via the rake task or command line interface. These + # directives can be overridden using the command line interface arguments or with ENV variables when using the rake + # task. + # + # Command Line Interface: + # teaspoon --driver=phantomjs --server-port=31337 --fail-fast=true --format=junit --suite=my_suite /spec/file_spec.js + # + # Rake: + # teaspoon DRIVER=phantomjs SERVER_PORT=31337 FAIL_FAST=true FORMATTERS=junit suite=my_suite + + # Specify which headless driver to use. Supports PhantomJS and Selenium Webdriver. + # + # Available: :phantomjs, :selenium, :capybara_webkit + # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS + # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver + # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit + #config.driver = :phantomjs + + # Specify additional options for the driver. + # + # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS + # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver + # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit + #config.driver_options = nil + + # Specify the timeout for the driver. Specs are expected to complete within this time frame or the run will be + # considered a failure. This is to avoid issues that can arise where tests stall. + #config.driver_timeout = 180 + + # Specify a server to use with Rack (e.g. thin, mongrel). If nil is provided Rack::Server is used. + #config.server = nil + + # Specify a port to run on a specific port, otherwise Teaspoon will use a random available port. + #config.server_port = nil + + # Timeout for starting the server in seconds. If your server is slow to start you may have to bump this, or you may + # want to lower this if you know it shouldn't take long to start. + #config.server_timeout = 20 + + # Force Teaspoon to fail immediately after a failing suite. Can be useful to make Teaspoon fail early if you have + # several suites, but in environments like CI this may not be desirable. + #config.fail_fast = true + + # Specify the formatters to use when outputting the results. + # Note: Output files can be specified by using `"junit>/path/to/output.xml"`. + # + # Available: :dot, :clean, :documentation, :json, :junit, :pride, :rspec_html, :snowday, :swayze_or_oprah, :tap, :tap_y, :teamcity + #config.formatters = [:dot] + + # Specify if you want color output from the formatters. + #config.color = true + + # Teaspoon pipes all console[log/debug/error] to $stdout. This is useful to catch places where you've forgotten to + # remove them, but in verbose applications this may not be desirable. + #config.suppress_log = false + + # COVERAGE REPORTS / THRESHOLD ASSERTIONS + # + # Coverage reports requires Istanbul (https://github.com/gotwarlost/istanbul) to add instrumentation to your code and + # display coverage statistics. + # + # Coverage configurations are similar to suites. You can define several, and use different ones under different + # conditions. + # + # To run with a specific coverage configuration + # - with the rake task: rake teaspoon USE_COVERAGE=[coverage_name] + # - with the cli: teaspoon --coverage=[coverage_name] + + # Specify that you always want a coverage configuration to be used. Otherwise, specify that you want coverage + # on the CLI. + # Set this to "true" or the name of your coverage config. + #config.use_coverage = nil + + # You can have multiple coverage configs by passing a name to config.coverage. + # e.g. config.coverage :ci do |coverage| + # The default coverage config name is :default. + config.coverage do |coverage| + # Which coverage reports Istanbul should generate. Correlates directly to what Istanbul supports. + # + # Available: text-summary, text, html, lcov, lcovonly, cobertura, teamcity + #coverage.reports = ["text-summary", "html"] + + # The path that the coverage should be written to - when there's an artifact to write to disk. + # Note: Relative to `config.root`. + #coverage.output_path = "coverage" + + # Assets to be ignored when generating coverage reports. Accepts an array of filenames or regular expressions. The + # default excludes assets from vendor, gems and support libraries. + #coverage.ignore = [%r{/lib/ruby/gems/}, %r{/vendor/assets/}, %r{/support/}, %r{/(.+)_helper.}] + + # Various thresholds requirements can be defined, and those thresholds will be checked at the end of a run. If any + # aren't met the run will fail with a message. Thresholds can be defined as a percentage (0-100), or nil. + #coverage.statements = nil + #coverage.functions = nil + #coverage.branches = nil + #coverage.lines = nil + end +end diff --git a/vendor/assets/javascripts/jasmine-fixture.js b/vendor/assets/javascripts/jasmine-fixture.js deleted file mode 100755 index 9980aec6ddb..00000000000 --- a/vendor/assets/javascripts/jasmine-fixture.js +++ /dev/null @@ -1,433 +0,0 @@ -/* jasmine-fixture - 1.3.1 - * Makes injecting HTML snippets into the DOM easy & clean! - * https://github.com/searls/jasmine-fixture - */ -(function() { - var createHTMLBlock, - __slice = [].slice; - - (function($) { - var ewwSideEffects, jasmineFixture, originalAffix, originalJasmineDotFixture, originalJasmineFixture, root, _, _ref; - root = (1, eval)('this'); - originalJasmineFixture = root.jasmineFixture; - originalJasmineDotFixture = (_ref = root.jasmine) != null ? _ref.fixture : void 0; - originalAffix = root.affix; - _ = function(list) { - return { - inject: function(iterator, memo) { - var item, _i, _len, _results; - _results = []; - for (_i = 0, _len = list.length; _i < _len; _i++) { - item = list[_i]; - _results.push(memo = iterator(memo, item)); - } - return _results; - } - }; - }; - root.jasmineFixture = function($) { - var $whatsTheRootOf, affix, create, jasmineFixture, noConflict; - affix = function(selectorOptions) { - return create.call(this, selectorOptions, true); - }; - create = function(selectorOptions, attach) { - var $top; - $top = null; - _(selectorOptions.split(/[ ](?![^\{]*\})(?=[^\]]*?(?:\[|$))/)).inject(function($parent, elementSelector) { - var $el; - if (elementSelector === ">") { - return $parent; - } - $el = createHTMLBlock($, elementSelector); - if (attach || $top) { - $el.appendTo($parent); - } - $top || ($top = $el); - return $el; - }, $whatsTheRootOf(this)); - return $top; - }; - noConflict = function() { - var currentJasmineFixture, _ref1; - currentJasmineFixture = jasmine.fixture; - root.jasmineFixture = originalJasmineFixture; - if ((_ref1 = root.jasmine) != null) { - _ref1.fixture = originalJasmineDotFixture; - } - root.affix = originalAffix; - return currentJasmineFixture; - }; - $whatsTheRootOf = function(that) { - if (that.jquery != null) { - return that; - } else if ($('#jasmine_content').length > 0) { - return $('#jasmine_content'); - } else { - return $('<div id="jasmine_content"></div>').appendTo('body'); - } - }; - jasmineFixture = { - affix: affix, - create: create, - noConflict: noConflict - }; - ewwSideEffects(jasmineFixture); - return jasmineFixture; - }; - ewwSideEffects = function(jasmineFixture) { - var _ref1; - if ((_ref1 = root.jasmine) != null) { - _ref1.fixture = jasmineFixture; - } - $.fn.affix = root.affix = jasmineFixture.affix; - return afterEach(function() { - return $('#jasmine_content').remove(); - }); - }; - if ($) { - return jasmineFixture = root.jasmineFixture($); - } else { - return root.affix = function() { - var nowJQueryExists; - nowJQueryExists = window.jQuery || window.$; - if (nowJQueryExists != null) { - jasmineFixture = root.jasmineFixture(nowJQueryExists); - return affix.call.apply(affix, [this].concat(__slice.call(arguments))); - } else { - throw new Error("jasmine-fixture requires jQuery to be defined at window.jQuery or window.$"); - } - }; - } - })(window.jQuery || window.$); - - createHTMLBlock = (function() { - var bindData, bindEvents, parseAttributes, parseClasses, parseContents, parseEnclosure, parseReferences, parseVariableScope, regAttr, regAttrDfn, regAttrs, regCBrace, regClass, regClasses, regData, regDatas, regEvent, regEvents, regExclamation, regId, regReference, regTag, regTagNotContent, regZenTagDfn; - createHTMLBlock = function($, ZenObject, data, functions, indexes) { - var ZenCode, arr, block, blockAttrs, blockClasses, blockHTML, blockId, blockTag, blocks, el, el2, els, forScope, indexName, inner, len, obj, origZenCode, paren, result, ret, zc, zo; - if ($.isPlainObject(ZenObject)) { - ZenCode = ZenObject.main; - } else { - ZenCode = ZenObject; - ZenObject = { - main: ZenCode - }; - } - origZenCode = ZenCode; - if (indexes === undefined) { - indexes = {}; - } - if (ZenCode.charAt(0) === "!" || $.isArray(data)) { - if ($.isArray(data)) { - forScope = ZenCode; - } else { - obj = parseEnclosure(ZenCode, "!"); - obj = obj.substring(obj.indexOf(":") + 1, obj.length - 1); - forScope = parseVariableScope(ZenCode); - } - while (forScope.charAt(0) === "@") { - forScope = parseVariableScope("!for:!" + parseReferences(forScope, ZenObject)); - } - zo = ZenObject; - zo.main = forScope; - el = $(); - if (ZenCode.substring(0, 5) === "!for:" || $.isArray(data)) { - if (!$.isArray(data) && obj.indexOf(":") > 0) { - indexName = obj.substring(0, obj.indexOf(":")); - obj = obj.substr(obj.indexOf(":") + 1); - } - arr = ($.isArray(data) ? data : data[obj]); - zc = zo.main; - if ($.isArray(arr) || $.isPlainObject(arr)) { - $.map(arr, function(value, index) { - var next; - zo.main = zc; - if (indexName !== undefined) { - indexes[indexName] = index; - } - if (!$.isPlainObject(value)) { - value = { - value: value - }; - } - next = createHTMLBlock($, zo, value, functions, indexes); - if (el.length !== 0) { - return $.each(next, function(index, value) { - return el.push(value); - }); - } - }); - } - if (!$.isArray(data)) { - ZenCode = ZenCode.substr(obj.length + 6 + forScope.length); - } else { - ZenCode = ""; - } - } else if (ZenCode.substring(0, 4) === "!if:") { - result = parseContents("!" + obj + "!", data, indexes); - if (result !== "undefined" || result !== "false" || result !== "") { - el = createHTMLBlock($, zo, data, functions, indexes); - } - ZenCode = ZenCode.substr(obj.length + 5 + forScope.length); - } - ZenObject.main = ZenCode; - } else if (ZenCode.charAt(0) === "(") { - paren = parseEnclosure(ZenCode, "(", ")"); - inner = paren.substring(1, paren.length - 1); - ZenCode = ZenCode.substr(paren.length); - zo = ZenObject; - zo.main = inner; - el = createHTMLBlock($, zo, data, functions, indexes); - } else { - blocks = ZenCode.match(regZenTagDfn); - block = blocks[0]; - if (block.length === 0) { - return ""; - } - if (block.indexOf("@") >= 0) { - ZenCode = parseReferences(ZenCode, ZenObject); - zo = ZenObject; - zo.main = ZenCode; - return createHTMLBlock($, zo, data, functions, indexes); - } - block = parseContents(block, data, indexes); - blockClasses = parseClasses($, block); - if (regId.test(block)) { - blockId = regId.exec(block)[1]; - } - blockAttrs = parseAttributes(block, data); - blockTag = (block.charAt(0) === "{" ? "span" : "div"); - if (ZenCode.charAt(0) !== "#" && ZenCode.charAt(0) !== "." && ZenCode.charAt(0) !== "{") { - blockTag = regTag.exec(block)[1]; - } - if (block.search(regCBrace) !== -1) { - blockHTML = block.match(regCBrace)[1]; - } - blockAttrs = $.extend(blockAttrs, { - id: blockId, - "class": blockClasses, - html: blockHTML - }); - el = $("<" + blockTag + ">", blockAttrs); - el.attr(blockAttrs); - el = bindEvents(block, el, functions); - el = bindData(block, el, data); - ZenCode = ZenCode.substr(blocks[0].length); - ZenObject.main = ZenCode; - } - if (ZenCode.length > 0) { - if (ZenCode.charAt(0) === ">") { - if (ZenCode.charAt(1) === "(") { - zc = parseEnclosure(ZenCode.substr(1), "(", ")"); - ZenCode = ZenCode.substr(zc.length + 1); - } else if (ZenCode.charAt(1) === "!") { - obj = parseEnclosure(ZenCode.substr(1), "!"); - forScope = parseVariableScope(ZenCode.substr(1)); - zc = obj + forScope; - ZenCode = ZenCode.substr(zc.length + 1); - } else { - len = Math.max(ZenCode.indexOf("+"), ZenCode.length); - zc = ZenCode.substring(1, len); - ZenCode = ZenCode.substr(len); - } - zo = ZenObject; - zo.main = zc; - els = $(createHTMLBlock($, zo, data, functions, indexes)); - els.appendTo(el); - } - if (ZenCode.charAt(0) === "+") { - zo = ZenObject; - zo.main = ZenCode.substr(1); - el2 = createHTMLBlock($, zo, data, functions, indexes); - $.each(el2, function(index, value) { - return el.push(value); - }); - } - } - ret = el; - return ret; - }; - bindData = function(ZenCode, el, data) { - var datas, i, split; - if (ZenCode.search(regDatas) === 0) { - return el; - } - datas = ZenCode.match(regDatas); - if (datas === null) { - return el; - } - i = 0; - while (i < datas.length) { - split = regData.exec(datas[i]); - if (split[3] === undefined) { - $(el).data(split[1], data[split[1]]); - } else { - $(el).data(split[1], data[split[3]]); - } - i++; - } - return el; - }; - bindEvents = function(ZenCode, el, functions) { - var bindings, fn, i, split; - if (ZenCode.search(regEvents) === 0) { - return el; - } - bindings = ZenCode.match(regEvents); - if (bindings === null) { - return el; - } - i = 0; - while (i < bindings.length) { - split = regEvent.exec(bindings[i]); - if (split[2] === undefined) { - fn = functions[split[1]]; - } else { - fn = functions[split[2]]; - } - $(el).bind(split[1], fn); - i++; - } - return el; - }; - parseAttributes = function(ZenBlock, data) { - var attrStrs, attrs, i, parts; - if (ZenBlock.search(regAttrDfn) === -1) { - return undefined; - } - attrStrs = ZenBlock.match(regAttrDfn); - attrs = {}; - i = 0; - while (i < attrStrs.length) { - parts = regAttr.exec(attrStrs[i]); - attrs[parts[1]] = ""; - if (parts[3] !== undefined) { - attrs[parts[1]] = parseContents(parts[3], data); - } - i++; - } - return attrs; - }; - parseClasses = function($, ZenBlock) { - var classes, clsString, i; - ZenBlock = ZenBlock.match(regTagNotContent)[0]; - if (ZenBlock.search(regClasses) === -1) { - return undefined; - } - classes = ZenBlock.match(regClasses); - clsString = ""; - i = 0; - while (i < classes.length) { - clsString += " " + regClass.exec(classes[i])[1]; - i++; - } - return $.trim(clsString); - }; - parseContents = function(ZenBlock, data, indexes) { - var html; - if (indexes === undefined) { - indexes = {}; - } - html = ZenBlock; - if (data === undefined) { - return html; - } - while (regExclamation.test(html)) { - html = html.replace(regExclamation, function(str, str2) { - var begChar, fn, val; - begChar = ""; - if (str.indexOf("!for:") > 0 || str.indexOf("!if:") > 0) { - return str; - } - if (str.charAt(0) !== "!") { - begChar = str.charAt(0); - str = str.substring(2, str.length - 1); - } - fn = new Function("data", "indexes", "var r=undefined;" + "with(data){try{r=" + str + ";}catch(e){}}" + "with(indexes){try{if(r===undefined)r=" + str + ";}catch(e){}}" + "return r;"); - val = unescape(fn(data, indexes)); - return begChar + val; - }); - } - html = html.replace(/\\./g, function(str) { - return str.charAt(1); - }); - return unescape(html); - }; - parseEnclosure = function(ZenCode, open, close, count) { - var index, ret; - if (close === undefined) { - close = open; - } - index = 1; - if (count === undefined) { - count = (ZenCode.charAt(0) === open ? 1 : 0); - } - if (count === 0) { - return; - } - while (count > 0 && index < ZenCode.length) { - if (ZenCode.charAt(index) === close && ZenCode.charAt(index - 1) !== "\\") { - count--; - } else { - if (ZenCode.charAt(index) === open && ZenCode.charAt(index - 1) !== "\\") { - count++; - } - } - index++; - } - ret = ZenCode.substring(0, index); - return ret; - }; - parseReferences = function(ZenCode, ZenObject) { - ZenCode = ZenCode.replace(regReference, function(str) { - var fn; - str = str.substr(1); - fn = new Function("objs", "var r=\"\";" + "with(objs){try{" + "r=" + str + ";" + "}catch(e){}}" + "return r;"); - return fn(ZenObject, parseReferences); - }); - return ZenCode; - }; - parseVariableScope = function(ZenCode) { - var forCode, rest, tag; - if (ZenCode.substring(0, 5) !== "!for:" && ZenCode.substring(0, 4) !== "!if:") { - return undefined; - } - forCode = parseEnclosure(ZenCode, "!"); - ZenCode = ZenCode.substr(forCode.length); - if (ZenCode.charAt(0) === "(") { - return parseEnclosure(ZenCode, "(", ")"); - } - tag = ZenCode.match(regZenTagDfn)[0]; - ZenCode = ZenCode.substr(tag.length); - if (ZenCode.length === 0 || ZenCode.charAt(0) === "+") { - return tag; - } else if (ZenCode.charAt(0) === ">") { - rest = ""; - rest = parseEnclosure(ZenCode.substr(1), "(", ")", 1); - return tag + ">" + rest; - } - return undefined; - }; - regZenTagDfn = /([#\.\@]?[\w-]+|\[([\w-!?=:"']+(="([^"]|\\")+")? {0,})+\]|\~[\w$]+=[\w$]+|&[\w$]+(=[\w$]+)?|[#\.\@]?!([^!]|\\!)+!){0,}(\{([^\}]|\\\})+\})?/i; - regTag = /(\w+)/i; - regId = /(?:^|\b)#([\w-!]+)/i; - regTagNotContent = /((([#\.]?[\w-]+)?(\[([\w!]+(="([^"]|\\")+")? {0,})+\])?)+)/i; - /* - See lookahead syntax (?!) at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp - */ - - regClasses = /(\.[\w-]+)(?!["\w])/g; - regClass = /\.([\w-]+)/i; - regReference = /(@[\w$_][\w$_\d]+)/i; - regAttrDfn = /(\[([\w-!]+(="?([^"]|\\")+"?)? {0,})+\])/ig; - regAttrs = /([\w-!]+(="([^"]|\\")+")?)/g; - regAttr = /([\w-!]+)(="?((([\w]+(\[.*?\])+)|[^"\]]|\\")+)"?)?/i; - regCBrace = /\{(([^\}]|\\\})+)\}/i; - regExclamation = /(?:([^\\]|^))!([^!]|\\!)+!/g; - regEvents = /\~[\w$]+(=[\w$]+)?/g; - regEvent = /\~([\w$]+)=([\w$]+)/i; - regDatas = /&[\w$]+(=[\w$]+)?/g; - regData = /&([\w$]+)(=([\w$]+))?/i; - return createHTMLBlock; - })(); - -}).call(this); |
