diff --git a/CHANGELOG b/CHANGELOG index 388fa2f8966..87baa6b6621 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,12 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.1.0 (unreleased) + - Make diff file view easier to use on mobile screens (Stan Hu) + - Add support for creating directories from Files page (Stan Hu) + - Allow removing of project without confirmation when JavaScript is disabled (Stan Hu) + - Support filtering by "Any" milestone or issue and fix "No Milestone" and "No Label" filters (Stan Hu) + - Improved performance of the trending projects page + - Improved performance of finding projects by their namespace - Fix bug where transferring a project would result in stale commit links (Stan Hu) - Include full path of source and target branch names in New Merge Request page (Stan Hu) - Add user preference to view activities as default dashboard (Stan Hu) @@ -10,6 +16,7 @@ v 8.1.0 (unreleased) - Move CI charts to project graphs area - Fix cases where Markdown did not render links in activity feed (Stan Hu) - Add first and last to pagination (Zeger-Jan van de Weg) + - Added Commit Status API - Show CI status on commit page - Show CI status on Your projects page and Starred projects page - Remove "Continuous Integration" page from dashboard @@ -33,6 +40,20 @@ v 8.1.0 (unreleased) - Add user preference to change layout width (Peter Göbel) - Use commit status in merge request widget as preffered source of CI status - Integrate CI commit and build pages into project pages + - Move CI services page to project settings area + - Add "Quick Submit" behavior to input fields throughout the application. Use + Cmd+Enter on Mac and Ctrl+Enter on Windows/Linux. + - Fix position of hamburger in header for smaller screens (Han Loong Liauw) + - Fix bug where Emojis in Markdown would truncate remaining text (Sakata Sinji) + - Persist filters when sorting on admin user page (Jerry Lukins) + - Add spellcheck=false to certain input fields + - Invalidate stored service password if the endpoint URL is changed + - Project names are not fully shown if group name is too big, even on group page view + - Apply new design for Files page + - Add "New Page" button to Wiki Pages tab (Stan Hu) + - Only render 404 page from /public + - Hide passwords from services API (Alex Lossent) + - Fix: Images cannot show when projects' path was changed v 8.0.4 - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu) @@ -41,11 +62,13 @@ v 8.0.4 - Remove CI token from build traces - Fix "Assign All" button on Runner admin page - Fix search in Files + - Add full project namespace to payload of system webhooks (Ricardo Band) v 8.0.3 - Fix URL shown in Slack notifications - Fix bug where projects would appear to be stuck in the forked import state (Stan Hu) - Fix Error 500 in creating merge requests with > 1000 diffs (Stan Hu) + - Add work_in_progress key to MR web hooks (Ben Boeckel) v 8.0.2 - Fix default avatar not rendering in network graph (Stan Hu) diff --git a/Gemfile b/Gemfile index 4938cbf8b80..967092994a6 100644 --- a/Gemfile +++ b/Gemfile @@ -22,20 +22,20 @@ gem "mysql2", '~> 0.3.16', group: :mysql gem "pg", '~> 0.18.2', group: :postgres # Authentication libraries -gem "devise", '~> 3.5.2' -gem "devise-async", '~> 0.9.0' -gem 'omniauth', "~> 1.2.2" -gem 'omniauth-google-oauth2', '~> 0.2.5' -gem 'omniauth-twitter', '~> 1.0.1' -gem 'omniauth-github', '~> 1.1.1' -gem 'omniauth-shibboleth', '~> 1.1.1' -gem 'omniauth-kerberos', '~> 0.2.0', group: :kerberos -gem 'omniauth-gitlab', '~> 1.0.0' -gem 'omniauth-bitbucket', '~> 0.0.2' -gem 'omniauth-saml', '~> 1.4.0' -gem 'doorkeeper', '~> 2.1.3' +gem 'devise', '~> 3.5.2' +gem 'devise-async', '~> 0.9.0' +gem 'doorkeeper', '~> 2.1.3' +gem 'omniauth', '~> 1.2.2' +gem 'omniauth-bitbucket', '~> 0.0.2' +gem 'omniauth-github', '~> 1.1.1' +gem 'omniauth-gitlab', '~> 1.0.0' +gem 'omniauth-google-oauth2', '~> 0.2.0' +gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos +gem 'omniauth-saml', '~> 1.4.0' +gem 'omniauth-shibboleth', '~> 1.2.0' +gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd' -gem "rack-oauth2", "~> 1.0.5" +gem 'rack-oauth2', '~> 1.0.5' # Two-factor authentication gem 'devise-two-factor', '~> 2.0.0' @@ -47,7 +47,7 @@ gem "browser", '~> 1.0.0' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.2.17' +gem "gitlab_git", '~> 7.2.18' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes @@ -65,9 +65,9 @@ gem 'gollum-lib', '~> 4.0.2' gem "gitlab-linguist", "~> 3.0.1", require: "linguist" # API -gem "grape", "~> 0.6.1" -gem "grape-entity", "~> 0.4.2" -gem 'rack-cors', '~> 0.2.9', require: 'rack/cors' +gem 'grape', '~> 0.6.1' +gem 'grape-entity', '~> 0.4.2' +gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' # Format dates and times # based on human-friendly examples @@ -80,7 +80,7 @@ gem 'enumerize', '~> 0.7.0' gem "kaminari", "~> 0.16.3" # HAML -gem "haml-rails", '~> 0.5.3' +gem "haml-rails", '~> 0.9.0' # Files attachments gem "carrierwave", '~> 0.9.0' @@ -128,7 +128,6 @@ gem 'after_commit_queue' gem 'acts-as-taggable-on', '~> 3.4' # Background jobs -gem 'slim', '~> 2.0.2' gem 'sinatra', '~> 1.4.4', require: nil gem 'sidekiq', '3.3.0' gem 'sidetiq', '~> 0.6.3' @@ -151,7 +150,7 @@ gem 'version_sorter', '~> 2.0.0' gem "redis-rails", '~> 4.0.0' # Campfire integration -gem 'tinder', '~> 1.9.2' +gem 'tinder', '~> 1.10.0' # HipChat integration gem 'hipchat', '~> 1.5.0' @@ -163,7 +162,7 @@ gem "gitlab-flowdock-git-hook", "~> 1.0.1" gem "gemnasium-gitlab-service", "~> 0.2" # Slack integration -gem "slack-notifier", "~> 1.0.0" +gem "slack-notifier", "~> 1.2.0" # Asana integration gem 'asana', '~> 0.0.6' @@ -291,7 +290,7 @@ gem 'newrelic-grape' gem 'octokit', '~> 3.7.0' -gem "mail_room", "~> 0.5.2" +gem "mail_room", "~> 0.6.0" gem 'email_reply_parser', '~> 0.5.8' @@ -300,9 +299,6 @@ gem 'activerecord-deprecated_finders', '~> 1.0.3' gem 'activerecord-session_store', '~> 0.1.0' gem "nested_form", '~> 0.3.2' -# Scheduled -gem 'whenever', '~> 0.8.4', require: false - # OAuth gem 'oauth2', '~> 1.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index 1dd56cd9c8c..58426a60683 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -105,7 +105,6 @@ GEM celluloid (0.16.0) timers (~> 4.0.0) charlock_holmes (0.6.9.4) - chronic (0.10.2) chunky_png (1.3.4) cliver (0.3.2) coderay (1.1.0) @@ -182,8 +181,8 @@ GEM factory_girl_rails (4.3.0) factory_girl (~> 4.3.0) railties (>= 3.0.0) - faraday (0.8.10) - multipart-post (~> 1.2.0) + faraday (0.9.2) + multipart-post (>= 1.2, < 3) faraday_middleware (0.10.0) faraday (>= 0.7.4, < 0.10) fastercsv (1.5.5) @@ -279,7 +278,7 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.1.1) gemojione (~> 2.0) - gitlab_git (7.2.17) + gitlab_git (7.2.18) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) @@ -330,12 +329,13 @@ GEM rspec (>= 2.14, < 4.0) haml (4.0.7) tilt - haml-rails (0.5.3) + haml-rails (0.9.0) actionpack (>= 4.0.1) activesupport (>= 4.0.1) - haml (>= 3.1, < 5.0) + haml (>= 4.0.6, < 5.0) + html2haml (>= 1.0.1) railties (>= 4.0.1) - hashie (2.1.2) + hashie (3.4.2) highline (1.6.21) hike (1.2.3) hipchat (1.5.2) @@ -345,6 +345,11 @@ GEM html-pipeline (1.11.0) activesupport (>= 2) nokogiri (~> 1.4) + html2haml (2.0.0) + erubis (~> 2.7.0) + haml (~> 4.0.0) + nokogiri (~> 1.6.0) + ruby_parser (~> 3.5) http-cookie (1.0.2) domain_name (~> 0.5) http_parser.rb (0.5.3) @@ -387,7 +392,7 @@ GEM systemu (~> 2.6.2) mail (2.6.3) mime-types (>= 1.16, < 3) - mail_room (0.5.2) + mail_room (0.6.0) method_source (0.8.2) mime-types (1.25.1) mimemagic (0.3.0) @@ -396,7 +401,7 @@ GEM mousetrap-rails (1.4.6) multi_json (1.11.2) multi_xml (0.5.5) - multipart-post (1.2.0) + multipart-post (2.0.0) mysql2 (0.3.20) nenv (0.2.0) nested_form (0.3.2) @@ -440,7 +445,7 @@ GEM omniauth-google-oauth2 (0.2.6) omniauth (> 1.0) omniauth-oauth2 (~> 1.1) - omniauth-kerberos (0.2.0) + omniauth-kerberos (0.3.0) omniauth-multipassword timfel-krb5-auth (~> 0.8) omniauth-multipassword (0.4.2) @@ -454,11 +459,11 @@ GEM omniauth-saml (1.4.1) omniauth (~> 1.1) ruby-saml (~> 1.0.0) - omniauth-shibboleth (1.1.2) + omniauth-shibboleth (1.2.1) omniauth (>= 1.0.0) - omniauth-twitter (1.0.1) - multi_json (~> 1.3) - omniauth-oauth (~> 1.0) + omniauth-twitter (1.2.1) + json (~> 1.3) + omniauth-oauth (~> 1.1) omniauth_crowd (2.2.3) activesupport nokogiri (>= 1.4.4) @@ -496,7 +501,7 @@ GEM rack (>= 0.4) rack-attack (4.3.0) rack - rack-cors (0.2.9) + rack-cors (0.4.0) rack-mini-profiler (0.9.7) rack (>= 1.1.3) rack-mount (0.8.3) @@ -666,10 +671,7 @@ GEM rack-protection (~> 1.4) tilt (>= 1.3, < 3) six (0.2.0) - slack-notifier (1.0.0) - slim (2.0.3) - temple (~> 0.6.6) - tilt (>= 1.3.3, < 2.1) + slack-notifier (1.2.1) slop (3.6.0) spinach (0.8.10) colorize @@ -705,7 +707,6 @@ GEM railties (>= 3.2.5, < 5) teaspoon-jasmine (2.2.0) teaspoon (>= 1.0.0) - temple (0.6.10) term-ansicolor (1.3.2) tins (~> 1.0) terminal-table (1.5.2) @@ -721,13 +722,13 @@ GEM timers (4.0.4) hitimes timfel-krb5-auth (0.8.3) - tinder (1.9.4) + tinder (1.10.1) eventmachine (~> 1.0) - faraday (~> 0.8.9) + faraday (~> 0.9.0) faraday_middleware (~> 0.9) - hashie (>= 1.0, < 3) + hashie (>= 1.0) json (~> 1.8.0) - mime-types (~> 1.19) + mime-types multi_json (~> 1.7) twitter-stream (~> 0.1) tins (1.6.0) @@ -770,9 +771,6 @@ GEM websocket-driver (0.6.2) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) - whenever (0.8.4) - activesupport (>= 2.3.4) - chronic (>= 0.6.3) wikicloth (0.8.1) builder expression_parser @@ -836,7 +834,7 @@ DEPENDENCIES gitlab-flowdock-git-hook (~> 1.0.1) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) - gitlab_git (~> 7.2.17) + gitlab_git (~> 7.2.18) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.0.2) @@ -845,7 +843,7 @@ DEPENDENCIES grape-entity (~> 0.4.2) growl guard-rspec (~> 4.2.0) - haml-rails (~> 0.5.3) + haml-rails (~> 0.9.0) hipchat (~> 1.5.0) html-pipeline (~> 1.11.0) httparty (~> 0.13.3) @@ -856,7 +854,7 @@ DEPENDENCIES jquery-ui-rails (~> 4.2.1) kaminari (~> 0.16.3) letter_opener (~> 1.1.2) - mail_room (~> 0.5.2) + mail_room (~> 0.6.0) minitest (~> 5.7.0) mousetrap-rails (~> 1.4.6) mysql2 (~> 0.3.16) @@ -870,11 +868,11 @@ DEPENDENCIES omniauth-bitbucket (~> 0.0.2) omniauth-github (~> 1.1.1) omniauth-gitlab (~> 1.0.0) - omniauth-google-oauth2 (~> 0.2.5) - omniauth-kerberos (~> 0.2.0) + omniauth-google-oauth2 (~> 0.2.0) + omniauth-kerberos (~> 0.3.0) omniauth-saml (~> 1.4.0) - omniauth-shibboleth (~> 1.1.1) - omniauth-twitter (~> 1.0.1) + omniauth-shibboleth (~> 1.2.0) + omniauth-twitter (~> 1.2.0) omniauth_crowd org-ruby (~> 0.9.12) paranoia (~> 2.0) @@ -883,7 +881,7 @@ DEPENDENCIES pry-rails quiet_assets (~> 1.0.2) rack-attack (~> 4.3.0) - rack-cors (~> 0.2.9) + rack-cors (~> 0.4.0) rack-mini-profiler (~> 0.9.0) rack-oauth2 (~> 1.0.5) rails (= 4.1.12) @@ -912,8 +910,7 @@ DEPENDENCIES simplecov (~> 0.10.0) sinatra (~> 1.4.4) six (~> 0.2.0) - slack-notifier (~> 1.0.0) - slim (~> 2.0.2) + slack-notifier (~> 1.2.0) spinach-rails (~> 0.2.1) spring (~> 1.3.6) spring-commands-rspec (~> 1.0.4) @@ -927,7 +924,7 @@ DEPENDENCIES teaspoon-jasmine (~> 2.2.0) test_after_commit (~> 0.2.2) thin (~> 1.6.1) - tinder (~> 1.9.2) + tinder (~> 1.10.0) turbolinks (~> 2.5.0) uglifier (~> 2.3.2) underscore-rails (~> 1.4.4) @@ -937,7 +934,6 @@ DEPENDENCIES version_sorter (~> 2.0.0) virtus (~> 1.0.1) webmock (~> 1.21.0) - whenever (~> 0.8.4) wikicloth (= 0.8.1) BUNDLED WITH diff --git a/PROCESS.md b/PROCESS.md index 1b6b3e7d32d..9f4b708d2b5 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -79,7 +79,11 @@ Thanks for the issue report but we only support issues for the latest stable ver ### Support requests and configuration questions -Thanks for your interest in GitLab. We don't use the issue tracker for support requests and configuration questions. Please use the \[support forum\]\(https://groups.google.com/forum/#!forum/gitlabhq), \[Stack Overflow\]\(http://stackoverflow.com/questions/tagged/gitlab), the #gitlab IRC channel on Freenode or the http://about.gitlab.com paid services for this purpose. Have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) for more information. +Thanks for your interest in GitLab. We don't use the issue tracker for support +requests and configuration questions. Please check our +\[getting help\]\(https://about.gitlab.com/getting-help/) page to see all of the available +support options. Also, have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) +for more information. ### Code format diff --git a/README.md b/README.md index 91855b42d29..52e2d977620 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ GitLab is a Ruby on Rails application that runs on the following software: - Ubuntu/Debian/CentOS/RHEL - Ruby (MRI) 2.1 - Git 1.7.10+ -- Redis 2.0+ +- Redis 2.4+ - MySQL or PostgreSQL For more information please see the [architecture documentation](http://doc.gitlab.com/ce/development/architecture.html). diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 8e987ac4e83..945ffb660e6 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -180,6 +180,7 @@ $ -> $('.navbar-toggle').on 'click', -> $('.header-content .title').toggle() $('.header-content .navbar-collapse').toggle() + $('.navbar-toggle').toggleClass('active') # Show/hide comments on diff $("body").on "click", ".js-toggle-diff-comments", (e) -> diff --git a/app/assets/javascripts/behaviors/quick_submit.js.coffee b/app/assets/javascripts/behaviors/quick_submit.js.coffee new file mode 100644 index 00000000000..4ec8531d580 --- /dev/null +++ b/app/assets/javascripts/behaviors/quick_submit.js.coffee @@ -0,0 +1,29 @@ +# Quick Submit behavior +# +# When an input field with the `js-quick-submit` class receives a "Meta+Enter" +# (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, its parent form is +# submitted. +# +#= require extensions/jquery +# +# ### Example Markup +# +#
+# +# +#
+# +$(document).on 'keydown.quick_submit', '.js-quick-submit', (e) -> + return if (e.originalEvent && e.originalEvent.repeat) || e.repeat + return unless e.keyCode == 13 # Enter + + if navigator.userAgent.match(/Macintosh/) + return unless (e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey) + else + return unless (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) + + e.preventDefault() + + $form = $(e.target).closest('form') + $form.find('input[type=submit], button[type=submit]').disable() + $form.submit() diff --git a/app/assets/javascripts/behaviors/requires_input.js.coffee b/app/assets/javascripts/behaviors/requires_input.js.coffee index 8318fe435b3..79d750d1847 100644 --- a/app/assets/javascripts/behaviors/requires_input.js.coffee +++ b/app/assets/javascripts/behaviors/requires_input.js.coffee @@ -34,6 +34,5 @@ $.fn.requiresInput = -> $form.on 'change input', fieldSelector, requireInput -# Triggered on standard document `ready` and on Turbolinks `page:load` events -$(document).on 'ready page:load', -> +$ -> $('form.js-requires-input').requiresInput() diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee index 3ab3ba66754..5b604adbbb1 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee +++ b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee @@ -47,6 +47,7 @@ class @BlobFileDropzone return this.on 'sending', (file, xhr, formData) -> + formData.append('new_branch', form.find('#new_branch').val()) formData.append('commit_message', form.find('#commit_message').val()) return diff --git a/app/assets/javascripts/ci/Chart.min.js b/app/assets/javascripts/ci/Chart.min.js deleted file mode 100644 index ab635881087..00000000000 --- a/app/assets/javascripts/ci/Chart.min.js +++ /dev/null @@ -1,39 +0,0 @@ -var Chart=function(s){function v(a,c,b){a=A((a-c.graphMin)/(c.steps*c.stepValue),1,0);return b*c.steps*a}function x(a,c,b,e){function h(){g+=f;var k=a.animation?A(d(g),null,0):1;e.clearRect(0,0,q,u);a.scaleOverlay?(b(k),c()):(c(),b(k));if(1>=g)D(h);else if("function"==typeof a.onAnimationComplete)a.onAnimationComplete()}var f=a.animation?1/A(a.animationSteps,Number.MAX_VALUE,1):1,d=B[a.animationEasing],g=a.animation?0:1;"function"!==typeof c&&(c=function(){});D(h)}function C(a,c,b,e,h,f){var d;a= -Math.floor(Math.log(e-h)/Math.LN10);h=Math.floor(h/(1*Math.pow(10,a)))*Math.pow(10,a);e=Math.ceil(e/(1*Math.pow(10,a)))*Math.pow(10,a)-h;a=Math.pow(10,a);for(d=Math.round(e/a);dc;)a=dc?c:!isNaN(parseFloat(b))&& -isFinite(b)&&a)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');");return c? -b(c):b}var r=this,B={linear:function(a){return a},easeInQuad:function(a){return a*a},easeOutQuad:function(a){return-1*a*(a-2)},easeInOutQuad:function(a){return 1>(a/=0.5)?0.5*a*a:-0.5*(--a*(a-2)-1)},easeInCubic:function(a){return a*a*a},easeOutCubic:function(a){return 1*((a=a/1-1)*a*a+1)},easeInOutCubic:function(a){return 1>(a/=0.5)?0.5*a*a*a:0.5*((a-=2)*a*a+2)},easeInQuart:function(a){return a*a*a*a},easeOutQuart:function(a){return-1*((a=a/1-1)*a*a*a-1)},easeInOutQuart:function(a){return 1>(a/=0.5)? -0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)},easeInQuint:function(a){return 1*(a/=1)*a*a*a*a},easeOutQuint:function(a){return 1*((a=a/1-1)*a*a*a*a+1)},easeInOutQuint:function(a){return 1>(a/=0.5)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)},easeInSine:function(a){return-1*Math.cos(a/1*(Math.PI/2))+1},easeOutSine:function(a){return 1*Math.sin(a/1*(Math.PI/2))},easeInOutSine:function(a){return-0.5*(Math.cos(Math.PI*a/1)-1)},easeInExpo:function(a){return 0==a?1:1*Math.pow(2,10*(a/1-1))},easeOutExpo:function(a){return 1== -a?1:1*(-Math.pow(2,-10*a/1)+1)},easeInOutExpo:function(a){return 0==a?0:1==a?1:1>(a/=0.5)?0.5*Math.pow(2,10*(a-1)):0.5*(-Math.pow(2,-10*--a)+2)},easeInCirc:function(a){return 1<=a?a:-1*(Math.sqrt(1-(a/=1)*a)-1)},easeOutCirc:function(a){return 1*Math.sqrt(1-(a=a/1-1)*a)},easeInOutCirc:function(a){return 1>(a/=0.5)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)},easeInElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);ea?-0.5*e*Math.pow(2,10* -(a-=1))*Math.sin((1*a-c)*2*Math.PI/b):0.5*e*Math.pow(2,-10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInBack:function(a){return 1*(a/=1)*a*(2.70158*a-1.70158)},easeOutBack:function(a){return 1*((a=a/1-1)*a*(2.70158*a+1.70158)+1)},easeInOutBack:function(a){var c=1.70158;return 1>(a/=0.5)?0.5*a*a*(((c*=1.525)+1)*a-c):0.5*((a-=2)*a*(((c*=1.525)+1)*a+c)+2)},easeInBounce:function(a){return 1-B.easeOutBounce(1-a)},easeOutBounce:function(a){return(a/=1)<1/2.75?1*7.5625*a*a:a<2/2.75?1*(7.5625*(a-=1.5/2.75)* -a+0.75):a<2.5/2.75?1*(7.5625*(a-=2.25/2.75)*a+0.9375):1*(7.5625*(a-=2.625/2.75)*a+0.984375)},easeInOutBounce:function(a){return 0.5>a?0.5*B.easeInBounce(2*a):0.5*B.easeOutBounce(2*a-1)+0.5}},q=s.canvas.width,u=s.canvas.height;window.devicePixelRatio&&(s.canvas.style.width=q+"px",s.canvas.style.height=u+"px",s.canvas.height=u*window.devicePixelRatio,s.canvas.width=q*window.devicePixelRatio,s.scale(window.devicePixelRatio,window.devicePixelRatio));this.PolarArea=function(a,c){r.PolarArea.defaults={scaleOverlay:!0, -scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce", -animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.PolarArea.defaults,c):r.PolarArea.defaults;return new G(a,b,s)};this.Radar=function(a,c){r.Radar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!1,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)", -scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,angleShowLineOut:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:12,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Radar.defaults,c):r.Radar.defaults;return new H(a,b,s)};this.Pie=function(a, -c){r.Pie.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.Pie.defaults,c):r.Pie.defaults;return new I(a,b,s)};this.Doughnut=function(a,c){r.Doughnut.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1, -onAnimationComplete:null};var b=c?y(r.Doughnut.defaults,c):r.Doughnut.defaults;return new J(a,b,s)};this.Line=function(a,c){r.Line.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0, -pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:2,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Line.defaults,c):r.Line.defaults;return new K(a,b,s)};this.Bar=function(a,c){r.Bar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'", -scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Bar.defaults,c):r.Bar.defaults;return new L(a,b,s)};var G=function(a,c,b){var e,h,f,d,g,k,j,l,m;g=Math.min.apply(Math,[q,u])/2;g-=Math.max.apply(Math,[0.5*c.scaleFontSize,0.5*c.scaleLineWidth]); -d=2*c.scaleFontSize;c.scaleShowLabelBackdrop&&(d+=2*c.scaleBackdropPaddingY,g-=1.5*c.scaleBackdropPaddingY);l=g;d=d?d:5;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;fe&&(e=a[f].value),a[f].valuel&&(l=h);g-=Math.max.apply(Math,[l,1.5*(c.pointLabelFontSize/2)]);g-=c.pointLabelFontSize;l=g=A(g,null,0);d=d?d:5;e=Number.MIN_VALUE; -h=Number.MAX_VALUE;for(f=0;fe&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]Math.PI?"right":"left";b.textBaseline="middle";b.fillText(a.labels[d],f,-h)}b.restore()},function(d){var e=2*Math.PI/a.datasets[0].data.length;b.save();b.translate(q/2,u/2);for(var g=0;gt?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]d?h:d;d+=10}r=q-d-t;m=Math.floor(r/(a.labels.length-1));n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0t?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]< -h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;for(e=0;ed?h:d;d+=10}r=q-d-t;m= -Math.floor(r/a.labels.length);s=(m-2*c.scaleGridLineWidth-2*c.barValueSpacing-(c.barDatasetSpacing*a.datasets.length-1)-(c.barStrokeWidth/2*a.datasets.length-1))/a.datasets.length;n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0 $('.badge-codes-block').toggleClass("hide") return false - -$(document).on 'click', '.sync-now', -> - $(this).find('i').addClass('fa-spin') diff --git a/app/assets/javascripts/line_highlighter.js.coffee b/app/assets/javascripts/line_highlighter.js.coffee index e604e6025c2..2254a3f91ae 100644 --- a/app/assets/javascripts/line_highlighter.js.coffee +++ b/app/assets/javascripts/line_highlighter.js.coffee @@ -6,7 +6,7 @@ # # ### Example Markup # -#
+#
#
#
# 1 @@ -53,7 +53,7 @@ class @LineHighlighter $.scrollTo("#L#{range[0]}", offset: -150) bindEvents: -> - $('#tree-content-holder').on 'mousedown', 'a[data-line-number]', @clickHandler + $('#blob-content-holder').on 'mousedown', 'a[data-line-number]', @clickHandler # While it may seem odd to bind to the mousedown event and then throw away # the click event, there is a method to our madness. @@ -62,7 +62,7 @@ class @LineHighlighter # active state even when the event is cancelled, resulting in an ugly border # around the link and/or a persisted underline text decoration. - $('#tree-content-holder').on 'click', 'a[data-line-number]', (event) -> + $('#blob-content-holder').on 'click', 'a[data-line-number]', (event) -> event.preventDefault() clickHandler: (event) => diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 4e56791bde4..3e77ea515f8 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -69,7 +69,7 @@ class @MergeRequestTabs scrollToElement: (container) -> if window.location.hash top = $(container + " " + window.location.hash).offset().top - $('body').scrollTo(top); + $('body').scrollTo(top) # Activate a tab based on the current action activateTab: (action) -> @@ -139,13 +139,16 @@ class @MergeRequestTabs @diffsLoaded = true @scrollToElement(".diffs") - toggleLoading: -> - $('.mr-loading-status .loading').toggle() + # Show or hide the loading spinner + # + # status - Boolean, true to show, false to hide + toggleLoading: (status) -> + $('.mr-loading-status .loading').toggle(status) _get: (options) -> defaults = { - beforeSend: @toggleLoading - complete: @toggleLoading + beforeSend: => @toggleLoading(true) + complete: => @toggleLoading(false) dataType: 'json' type: 'GET' } diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 4b9f0d68912..ea75c656bcc 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -63,12 +63,6 @@ class @Notes # fetch notes when tab becomes visible $(document).on "visibilitychange", @visibilityChange - # Chrome doesn't fire keypress or keyup for Command+Enter, so we need keydown. - $(document).on 'keydown', '.js-note-text', (e) -> - return if e.originalEvent.repeat - if e.keyCode == 10 || ((e.metaKey || e.ctrlKey) && e.keyCode == 13) - $(@).closest('form').submit() - cleanBinding: -> $(document).off "ajax:success", ".js-main-target-form" $(document).off "ajax:success", ".js-discussion-note-form" @@ -82,7 +76,6 @@ class @Notes $(document).off "click", ".js-discussion-reply-button" $(document).off "click", ".js-add-diff-note-button" $(document).off "visibilitychange" - $(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/stylesheets/application.scss b/app/assets/stylesheets/application.scss index d9ede637944..7b060ce4853 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -11,59 +11,41 @@ *= require cal-heatmap */ - -@import "base/fonts"; -@import "base/variables"; -@import "base/mixins"; -@import "base/layout"; - - -/** - * Customized Twitter bootstrap +/* + * Welcome to GitLab css! + * If you need to add or modify UI component that is common for many pages + * like a table or typography then make changes in the framework/ directory. + * If you need to add unique style that should affect only one page - use pages/ + * directory. */ -@import 'base/gl_variables'; -@import 'base/gl_bootstrap'; -/** +/* + * GitLab UI framework + */ +@import "framework"; + +/* * NProgress load bar css */ @import 'nprogress'; @import 'nprogress-bootstrap'; -/** +/* * Font icons - * */ @import "font-awesome"; -/** - * UI themes: - */ -@import "themes/**/*"; - -/** - * Generic css (forms, nav etc): - */ -@import "generic/**/*"; - -/** +/* * Page specific styles (issues, projects etc): */ - @import "pages/**/*"; -/** +/* * Code highlight */ @import "highlight/**/*"; -/** +/* * Styles for JS behaviors. */ -@import "behaviors.scss"; - -/** - * CI specific styles: - */ -@import "ci/**/*"; - +@import "behaviors.scss"; \ No newline at end of file diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss deleted file mode 100644 index befd63832d5..00000000000 --- a/app/assets/stylesheets/base/variables.scss +++ /dev/null @@ -1,46 +0,0 @@ -$hover: #FFFAF1; -$gl-text-color: #54565B; -$gl-text-green: #4A2; -$gl-text-red: #D12F19; -$gl-text-orange: #D90; -$gl-header-color: #4c4e54; -$gl-link-color: #333c48; -$md-text-color: #444; -$md-link-color: #3084bb; -$nprogress-color: #c0392b; -$gl-font-size: 15px; -$list-font-size: 15px; -$sidebar_collapsed_width: 62px; -$sidebar_width: 230px; -$avatar_radius: 50%; -$code_font_size: 13px; -$code_line_height: 1.5; -$border-color: #dce0e6; -$background-color: #F7F8FA; -$header-height: 58px; -$fixed-layout-width: 1200px; -$gl-gray: #7f8fa4; -$gl-padding: 16px; -$gl-avatar-size: 46px; - - -/* - * State colors: - */ -$gl-primary: #446e9b; -$gl-success: #44c679; -$gl-info: #00aaff; -$gl-warning: #EB9532; -$gl-danger: #d9534f; - -/* - * Commit Diff Colors - */ -$added: #63c363; -$deleted: #f77; - -/* - * Fonts - */ -$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; -$regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif; diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss new file mode 100644 index 00000000000..1ec9d2fd84f --- /dev/null +++ b/app/assets/stylesheets/framework.scss @@ -0,0 +1,33 @@ +@import "framework/fonts"; +@import "framework/variables"; +@import "framework/mixins"; +@import "framework/layout"; +@import 'framework/tw_bootstrap_variables'; +@import 'framework/tw_bootstrap'; + +@import "framework/avatar.scss"; +@import "framework/blocks.scss"; +@import "framework/buttons.scss"; +@import "framework/calendar.scss"; +@import "framework/callout.scss"; +@import "framework/common.scss"; +@import "framework/files.scss"; +@import "framework/filters.scss"; +@import "framework/flash.scss"; +@import "framework/forms.scss"; +@import "framework/gfm.scss"; +@import "framework/gitlab-theme.scss"; +@import "framework/header.scss"; +@import "framework/highlight.scss"; +@import "framework/issue_box.scss"; +@import "framework/jquery.scss"; +@import "framework/lists.scss"; +@import "framework/markdown_area.scss"; +@import "framework/mobile.scss"; +@import "framework/pagination.scss"; +@import "framework/selects.scss"; +@import "framework/sidebar.scss"; +@import "framework/tables.scss"; +@import "framework/timeline.scss"; +@import "framework/typography.scss"; +@import "framework/zen.scss"; diff --git a/app/assets/stylesheets/generic/avatar.scss b/app/assets/stylesheets/framework/avatar.scss similarity index 100% rename from app/assets/stylesheets/generic/avatar.scss rename to app/assets/stylesheets/framework/avatar.scss diff --git a/app/assets/stylesheets/generic/blocks.scss b/app/assets/stylesheets/framework/blocks.scss similarity index 100% rename from app/assets/stylesheets/generic/blocks.scss rename to app/assets/stylesheets/framework/blocks.scss diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss new file mode 100644 index 00000000000..e5f0c0ad9ef --- /dev/null +++ b/app/assets/stylesheets/framework/buttons.scss @@ -0,0 +1,171 @@ +@mixin btn-default { + @include border-radius(2px); + border-width: 1px; + border-style: solid; + text-transform: uppercase; + font-size: 13px; + font-weight: 600; + line-height: 18px; + padding: 11px $gl-padding; + letter-spacing: .4px; + + &:focus, + &:active { + outline: none; + @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); + } +} + +@mixin btn-middle { + @include btn-default; + @include border-radius(2px); + padding: 11px 24px; +} + +@mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) { + background-color: $light; + border-color: $border-light; + color: $color; + + &:hover, + &:focus { + background-color: $normal; + border-color: $border-normal; + color: $color; + } + + &:active { + @include box-shadow (inset 0 0 4px rgba(0, 0, 0, 0.12)); + + background-color: $dark; + border-color: $border-dark; + color: $color; + } +} + +@mixin btn-green { + @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, #FFFFFF); +} + +@mixin btn-blue { + @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #FFFFFF); +} + +@mixin btn-orange { + @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #FFFFFF); +} + +@mixin btn-red { + @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, #FFFFFF); +} + +@mixin btn-gray { + @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, #313236); +} + +@mixin btn-white { + @include btn-color($white-light, $border-white-light, $white-normal, $border-white-normal, $white-dark, $border-white-dark, #313236); +} + +.btn { + @include btn-default; + @include btn-white; + + &.btn-sm { + padding: 5px 10px; + } + + &.btn-xs { + padding: 1px 5px; + } + + &.btn-success, + &.btn-new, + &.btn-create, + &.btn-save, + &.btn-green { + @include btn-green; + } + + &.btn-gray { + @include btn-gray; + } + + &.btn-primary, + &.btn-info { + @include btn-blue; + } + + &.btn-warning { + @include btn-orange; + } + + &.btn-danger, + &.btn-remove, + &.btn-red { + @include btn-red; + } + + &.btn-cancel { + float: right; + } + + &.btn-close { + color: $gl-danger; + border-color: $gl-danger; + &:hover { + color: #B94A48; + } + } + + &.btn-reopen { + color: $gl-success; + border-color: $gl-success; + &:hover { + color: #468847; + } + } + + &.btn-grouped { + margin-right: 7px; + float: left; + &:last-child { + margin-right: 0px; + } + } +} + +.btn-block { + width: 100%; + margin: 0; + margin-bottom: 15px; + &.btn { + padding: 6px 0; + } +} + +.btn-group { + &.btn-grouped { + margin-right: 7px; + float: left; + &:last-child { + margin-right: 0px; + } + } +} + +.btn-group-next { + .btn { + padding: 9px 0px; + font-size: 15px; + color: #7f8fa4; + border-color: #e7e9ed; + width: 140px; + + &.active { + border-color: $gl-info; + background: $gl-info; + color: #fff; + } + } +} diff --git a/app/assets/stylesheets/generic/calendar.scss b/app/assets/stylesheets/framework/calendar.scss similarity index 100% rename from app/assets/stylesheets/generic/calendar.scss rename to app/assets/stylesheets/framework/calendar.scss diff --git a/app/assets/stylesheets/generic/callout.scss b/app/assets/stylesheets/framework/callout.scss similarity index 100% rename from app/assets/stylesheets/generic/callout.scss rename to app/assets/stylesheets/framework/callout.scss diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/framework/common.scss similarity index 99% rename from app/assets/stylesheets/generic/common.scss rename to app/assets/stylesheets/framework/common.scss index 45e284542d2..e1a1793be9c 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -394,3 +394,7 @@ table { .dropzone .dz-preview .dz-progress .dz-upload { background: $gl-success !important; } + +.space-right { + margin-right: 10px; +} diff --git a/app/assets/stylesheets/generic/files.scss b/app/assets/stylesheets/framework/files.scss similarity index 100% rename from app/assets/stylesheets/generic/files.scss rename to app/assets/stylesheets/framework/files.scss diff --git a/app/assets/stylesheets/generic/filters.scss b/app/assets/stylesheets/framework/filters.scss similarity index 100% rename from app/assets/stylesheets/generic/filters.scss rename to app/assets/stylesheets/framework/filters.scss diff --git a/app/assets/stylesheets/generic/flash.scss b/app/assets/stylesheets/framework/flash.scss similarity index 100% rename from app/assets/stylesheets/generic/flash.scss rename to app/assets/stylesheets/framework/flash.scss diff --git a/app/assets/stylesheets/base/fonts.scss b/app/assets/stylesheets/framework/fonts.scss similarity index 100% rename from app/assets/stylesheets/base/fonts.scss rename to app/assets/stylesheets/framework/fonts.scss diff --git a/app/assets/stylesheets/generic/forms.scss b/app/assets/stylesheets/framework/forms.scss similarity index 86% rename from app/assets/stylesheets/generic/forms.scss rename to app/assets/stylesheets/framework/forms.scss index 4282832e2bf..0edfe24f195 100644 --- a/app/assets/stylesheets/generic/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -29,12 +29,6 @@ input[type='text'].danger { border-top: 1px solid $border-color; } -@media (min-width: $screen-sm-min) { - .form-actions { - padding-left: 17%; - } -} - label { &.control-label { @extend .col-sm-2; @@ -84,3 +78,17 @@ label { .wiki-content { margin-top: 35px; } + +.form-group .control-label { + font-weight: normal; +} + +.form-control::-webkit-input-placeholder { + color: #7f8fa4; +} + +.input-group { + .input-group-addon { + background-color: #f7f8fa; + } +} diff --git a/app/assets/stylesheets/generic/gfm.scss b/app/assets/stylesheets/framework/gfm.scss similarity index 95% rename from app/assets/stylesheets/generic/gfm.scss rename to app/assets/stylesheets/framework/gfm.scss index bd9200ace23..5ae0520fd7b 100644 --- a/app/assets/stylesheets/generic/gfm.scss +++ b/app/assets/stylesheets/framework/gfm.scss @@ -22,4 +22,5 @@ .gfm-commit, .gfm-commit_range { font-family: $monospace_font; + font-size: 90%; } diff --git a/app/assets/stylesheets/themes/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss similarity index 100% rename from app/assets/stylesheets/themes/gitlab-theme.scss rename to app/assets/stylesheets/framework/gitlab-theme.scss diff --git a/app/assets/stylesheets/generic/header.scss b/app/assets/stylesheets/framework/header.scss similarity index 96% rename from app/assets/stylesheets/generic/header.scss rename to app/assets/stylesheets/framework/header.scss index 543ce41ab52..91e6975e269 100644 --- a/app/assets/stylesheets/generic/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -50,15 +50,17 @@ header { .navbar-toggle { color: #666; - margin: 0; + margin: 6px 0; border-radius: 0; position: absolute; right: 2px; - top: 15px; &:hover { background-color: #EEE; } + &.active { + color: #7f8fa4; + } } } } @@ -87,6 +89,7 @@ header { .navbar-collapse { float: right; + border-top: none; } } diff --git a/app/assets/stylesheets/generic/highlight.scss b/app/assets/stylesheets/framework/highlight.scss similarity index 100% rename from app/assets/stylesheets/generic/highlight.scss rename to app/assets/stylesheets/framework/highlight.scss diff --git a/app/assets/stylesheets/generic/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss similarity index 94% rename from app/assets/stylesheets/generic/issue_box.scss rename to app/assets/stylesheets/framework/issue_box.scss index b1fb87a6830..93377e45e70 100644 --- a/app/assets/stylesheets/generic/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -5,7 +5,7 @@ */ .issue-box { - @include border-radius(3px); + @include border-radius(2px); display: inline-block; padding: 10px $gl-padding; diff --git a/app/assets/stylesheets/generic/jquery.scss b/app/assets/stylesheets/framework/jquery.scss similarity index 100% rename from app/assets/stylesheets/generic/jquery.scss rename to app/assets/stylesheets/framework/jquery.scss diff --git a/app/assets/stylesheets/base/layout.scss b/app/assets/stylesheets/framework/layout.scss similarity index 88% rename from app/assets/stylesheets/base/layout.scss rename to app/assets/stylesheets/framework/layout.scss index b91c15d8910..c7b3b60e769 100644 --- a/app/assets/stylesheets/base/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -5,6 +5,7 @@ html { body { padding-top: $header-height; + text-rendering: geometricPrecision; } } diff --git a/app/assets/stylesheets/generic/lists.scss b/app/assets/stylesheets/framework/lists.scss similarity index 95% rename from app/assets/stylesheets/generic/lists.scss rename to app/assets/stylesheets/framework/lists.scss index 3bfed8de772..c5764c36597 100644 --- a/app/assets/stylesheets/generic/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -117,8 +117,12 @@ ul.content-list { } .controls { - padding-top: 10px; + padding-top: 4px; float: right; + + .btn { + padding: 10px 14px; + } } } } diff --git a/app/assets/stylesheets/generic/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss similarity index 100% rename from app/assets/stylesheets/generic/markdown_area.scss rename to app/assets/stylesheets/framework/markdown_area.scss diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/framework/mixins.scss similarity index 55% rename from app/assets/stylesheets/base/mixins.scss rename to app/assets/stylesheets/framework/mixins.scss index c74a6d39824..089e6958eeb 100644 --- a/app/assets/stylesheets/base/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -54,147 +54,6 @@ @include box-shadow(0 0 0 3px #f1f1f1); } -@mixin md-typography { - color: $md-text-color; - - a { - color: $md-link-color; - } - - img { - max-width: 100%; - } - - *:first-child { - margin-top: 0; - } - - code { - font-family: $monospace_font; - white-space: pre; - word-wrap: normal; - padding: 1px 2px; - } - - kbd { - display: inline-block; - padding: 3px 5px; - font-size: 11px; - line-height: 10px; - color: #555; - vertical-align: middle; - background-color: #FCFCFC; - border-width: 1px; - border-style: solid; - border-color: #CCC #CCC #BBB; - border-image: none; - border-radius: 3px; - box-shadow: 0px -1px 0px #BBB inset; - } - - h1 { - font-size: 1.3em; - font-weight: 600; - margin: 24px 0 12px 0; - padding: 0 0 10px 0; - border-bottom: 1px solid #e7e9ed; - color: #313236; - } - - h2 { - font-size: 1.2em; - font-weight: 600; - margin: 24px 0 12px 0; - color: #313236; - } - - h3 { - margin: 24px 0 12px 0; - font-size: 1.25em; - } - - h4 { - margin: 24px 0 12px 0; - font-size: 1.1em; - } - - h5 { - margin: 24px 0 12px 0; - font-size: 1em; - } - - h6 { - margin: 24px 0 12px 0; - font-size: 0.90em; - } - - blockquote { - padding: 8px 21px; - margin: 12px 0 12px; - border-left: 3px solid #e7e9ed; - } - - blockquote p { - color: #7f8fa4 !important; - font-size: 15px; - line-height: 1.5; - } - - p { - color:#5c5d5e; - margin:6px 0 0 0; - } - - table { - @extend .table; - @extend .table-bordered; - margin: 12px 0 12px 0; - color: #5c5d5e; - th { - background: #f8fafc; - } - } - - pre { - margin: 12px 0 12px 0 !important; - background-color: #f8fafc !important; - font-size: 13px !important; - color: #5b6169 !important; - line-height: 1.6em !important; - @include border-radius(2px); - } - - p > code { - font-weight: inherit; - } - - - ul { - color: #5c5d5e; - } - - li { - line-height: 1.6em; - } - - a[href*="/uploads/"], a[href*="storage.googleapis.com/google-code-attachments/"] { - &:before { - margin-right: 4px; - - font: normal normal normal 14px/1 FontAwesome; - font-size: inherit; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - content: "\f0c6"; - } - - &:hover:before { - text-decoration: none; - } - } -} - - @mixin str-truncated($max_width: 82%) { display: inline-block; overflow: hidden; diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/framework/mobile.scss similarity index 91% rename from app/assets/stylesheets/generic/mobile.scss rename to app/assets/stylesheets/framework/mobile.scss index 36ae126f865..cea47fba192 100644 --- a/app/assets/stylesheets/generic/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -23,7 +23,7 @@ margin-right: 0; } - .issues-filters, + .issues-details-filters, .dash-projects-filters, .check-all-holder { display: none; @@ -83,6 +83,7 @@ .center-top-menu { height: 45px; + margin-bottom: 30px; li a { font-size: 14px; @@ -90,9 +91,11 @@ } } - .projects-search-form { - margin: 0 -5px !important; + .activity-filter-block { + display: none; + } + .projects-search-form { .btn { display: none; } @@ -100,6 +103,11 @@ } @media (max-width: $screen-sm-max) { + .page-with-sidebar .content-wrapper { + padding: 0; + padding-top: 1px; + } + .issues-filters { .milestone-filter, .labels-filter { display: none; diff --git a/app/assets/stylesheets/generic/pagination.scss b/app/assets/stylesheets/framework/pagination.scss similarity index 100% rename from app/assets/stylesheets/generic/pagination.scss rename to app/assets/stylesheets/framework/pagination.scss diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/framework/selects.scss similarity index 69% rename from app/assets/stylesheets/generic/selects.scss rename to app/assets/stylesheets/framework/selects.scss index f0860de1c49..cba621635b6 100644 --- a/app/assets/stylesheets/generic/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -8,7 +8,7 @@ font-size: $gl-font-size; line-height: 1.42857143; - @include border-radius(4px); + @include border-radius(2px); .select2-arrow { background: #FFF; @@ -18,8 +18,39 @@ } } +.select2-container .select2-choice, .select2-container.select2-drop-above .select2-choice{ + color: #7f8fa4; + border: 1px solid #e7e9ed; +} + +.select2-drop { + @include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px); + @include border-radius (0px); + + padding: 16px; + border: none !important; +} + +.select2-results .select2-result-label { + padding: 16px; +} + +.select2-drop{ + color: #7f8fa4; +} + +.select2-highlighted { + background: #3084bb !important; +} + +.select2-results li.select2-result-with-children > .select2-result-label { + font-weight: 600; + color: #313236; +} + + .select2-container-multi .select2-choices { - @include border-radius(4px); + @include border-radius(2px); border-color: #CCC; } @@ -63,7 +94,7 @@ .ajax-users-dropdown, .ajax-project-users-dropdown { .select2-search { - padding-top: 4px; + padding-top: 2px; } } @@ -97,9 +128,6 @@ } .user-name { } - .user-username { - color: #999; - } } .namespace-result { @@ -114,5 +142,5 @@ } .ajax-users-dropdown { - min-width: 225px !important; + min-width: 250px !important; } diff --git a/app/assets/stylesheets/generic/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss similarity index 100% rename from app/assets/stylesheets/generic/sidebar.scss rename to app/assets/stylesheets/framework/sidebar.scss diff --git a/app/assets/stylesheets/generic/tables.scss b/app/assets/stylesheets/framework/tables.scss similarity index 54% rename from app/assets/stylesheets/generic/tables.scss rename to app/assets/stylesheets/framework/tables.scss index a66e45577de..789b34020c1 100644 --- a/app/assets/stylesheets/generic/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -1,5 +1,21 @@ table { &.table { + .dropdown-menu a { + text-decoration: none; + } + + .success, + .warning, + .danger, + .info { + color: #fff; + + a:not(.btn) { + text-decoration: underline; + color: #fff; + } + } + tr { td, th { padding: 8px 10px; @@ -12,7 +28,7 @@ table { border-bottom: 1px solid $border-color !important; } td { - border-color: #F1F1F1 !important; + border-color: $table-border-color !important; border-bottom: 1px solid; } } diff --git a/app/assets/stylesheets/generic/timeline.scss b/app/assets/stylesheets/framework/timeline.scss similarity index 93% rename from app/assets/stylesheets/generic/timeline.scss rename to app/assets/stylesheets/framework/timeline.scss index 74bbaabad39..bf21d7fce76 100644 --- a/app/assets/stylesheets/generic/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -10,8 +10,8 @@ margin-left: -$gl-padding; margin-right: -$gl-padding; color: $gl-gray; - border-bottom: 1px solid #f1f2f4; - border-right: 1px solid #f1f2f4; + border-bottom: 1px solid #ECEEF1; + border-right: 1px solid #ECEEF1; &:last-child { border-bottom: none; diff --git a/app/assets/stylesheets/base/gl_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss similarity index 92% rename from app/assets/stylesheets/base/gl_bootstrap.scss rename to app/assets/stylesheets/framework/tw_bootstrap.scss index eb8d23d6453..99d028d1228 100644 --- a/app/assets/stylesheets/base/gl_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -32,8 +32,6 @@ @import "bootstrap/pager"; @import "bootstrap/labels"; @import "bootstrap/badges"; -@import "bootstrap/jumbotron"; -@import "bootstrap/thumbnails"; @import "bootstrap/alerts"; @import "bootstrap/progress-bars"; @import "bootstrap/list-group"; @@ -251,23 +249,3 @@ .text-info:hover { color: $brand-info; } - -// Tables ===================================================================== - -table.table { - .dropdown-menu a { - text-decoration: none; - } - - .success, - .warning, - .danger, - .info { - color: #fff; - - a:not(.btn) { - text-decoration: underline; - color: #fff; - } - } -} diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss similarity index 97% rename from app/assets/stylesheets/base/gl_variables.scss rename to app/assets/stylesheets/framework/tw_bootstrap_variables.scss index 7378d404008..63868a34e2a 100644 --- a/app/assets/stylesheets/base/gl_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -22,8 +22,8 @@ $brand-info: $gl-info; $brand-warning: $gl-warning; $brand-danger: $gl-danger; -$border-radius-base: 3px !default; -$border-radius-large: 5px !default; +$border-radius-base: 2px !default; +$border-radius-large: 2px !default; $border-radius-small: 2px !default; @@ -156,3 +156,5 @@ $nav-link-padding: 13px $gl-padding; $pre-bg: #f8fafc !default; $pre-color: $gl-gray !default; $pre-border-color: #e7e9ed; + +$table-bg-accent: $background-color; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss new file mode 100644 index 00000000000..bf36f96cc97 --- /dev/null +++ b/app/assets/stylesheets/framework/typography.scss @@ -0,0 +1,271 @@ +@mixin md-typography { + color: $md-text-color; + + a { + color: $md-link-color; + } + + img { + max-width: 100%; + } + + *:first-child { + margin-top: 0; + } + + code { + font-family: $monospace_font; + white-space: pre; + word-wrap: normal; + padding: 1px 2px; + } + + kbd { + display: inline-block; + padding: 3px 5px; + font-size: 11px; + line-height: 10px; + color: #555; + vertical-align: middle; + background-color: #FCFCFC; + border-width: 1px; + border-style: solid; + border-color: #CCC #CCC #BBB; + border-image: none; + border-radius: 3px; + box-shadow: 0px -1px 0px #BBB inset; + } + + h1 { + font-size: 1.3em; + font-weight: 600; + margin: 24px 0 12px 0; + padding: 0 0 10px 0; + border-bottom: 1px solid #e7e9ed; + color: #313236; + } + + h2 { + font-size: 1.2em; + font-weight: 600; + margin: 24px 0 12px 0; + color: #313236; + } + + h3 { + margin: 24px 0 12px 0; + font-size: 1.25em; + } + + h4 { + margin: 24px 0 12px 0; + font-size: 1.1em; + } + + h5 { + margin: 24px 0 12px 0; + font-size: 1em; + } + + h6 { + margin: 24px 0 12px 0; + font-size: 0.90em; + } + + blockquote { + padding: 8px 21px; + margin: 12px 0 12px; + border-left: 3px solid #e7e9ed; + } + + blockquote p { + color: #7f8fa4 !important; + font-size: 15px; + line-height: 1.5; + } + + p { + color:#5c5d5e; + margin:6px 0 0 0; + } + + table { + @extend .table; + @extend .table-bordered; + margin: 12px 0 12px 0; + color: #5c5d5e; + th { + background: #f8fafc; + } + } + + pre { + margin: 12px 0 12px 0 !important; + background-color: #f8fafc !important; + font-size: 13px !important; + color: #5b6169 !important; + line-height: 1.6em !important; + @include border-radius(2px); + } + + p > code { + font-weight: inherit; + } + + + ul { + color: #5c5d5e; + } + + li { + line-height: 1.6em; + } + + a[href*="/uploads/"], a[href*="storage.googleapis.com/google-code-attachments/"] { + &:before { + margin-right: 4px; + + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + content: "\f0c6"; + } + + &:hover:before { + text-decoration: none; + } + } +} + + +/** + * Headers + * + */ +body { + text-rendering:optimizeLegibility; + -webkit-text-shadow: rgba(255,255,255,0.01) 0 0 1px; +} + +.page-title { + margin-top: 0px; + line-height: 1.3; + font-size: 1.25em; + font-weight: 600; +} + +.page-title-empty { + margin-top: 0px; + line-height: 1.3; + font-size: 1.25em; + font-weight: 600; + margin: 12px 7px 12px 7px; +} + +h1, h2, h3, h4, h5, h6 { + color: $gl-header-color; + font-weight: 500; +} + +/** CODE **/ +pre { + font-family: $monospace_font; + + &.dark { + background: #333; + color: $background-color; + } + + &.plain-readme { + background: none; + border: none; + padding: 0; + margin: 0; + font-size: 14px; + } +} + +.monospace { + font-family: $monospace_font; +} + +code { + &.key-fingerprint { + background: $body-bg; + color: $text-color; + } +} + +a > code { + color: $link-color; +} + +/** + * Wiki typography + * + */ +.wiki { + @include md-typography; + + word-wrap: break-word; + padding: 7px; + + /* Link to current header. */ + h1, h2, h3, h4, h5, h6 { + position: relative; + + a.anchor { + // Setting `display: none` would prevent the anchor being scrolled to, so + // instead we set the height to 0 and it gets updated on hover. + height: 0; + } + + &:hover > a.anchor { + $size: 16px; + position: absolute; + right: 100%; + top: 50%; + margin-top: -$size/2; + margin-right: 0px; + padding-right: 20px; + display: inline-block; + width: $size; + height: $size; + background-image: image-url("icon-link.png"); + background-size: contain; + background-repeat: no-repeat; + } + } + + ul,ol { + padding: 0; + margin: 6px 0 6px 18px !important; + } + ol { + color: #5c5d5e; + } +} + +.md-area { + @include md-typography; +} + +.md { + @include md-typography; +} + +/** + * Textareas intended for GFM + * + */ +textarea.js-gfm-input { + font-family: $monospace_font; +} + +.md-preview { +} + +.strikethrough { + text-decoration: line-through; +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss new file mode 100644 index 00000000000..91954683c3e --- /dev/null +++ b/app/assets/stylesheets/framework/variables.scss @@ -0,0 +1,99 @@ +$hover: #FFFAF1; +$gl-text-color: #54565B; +$gl-text-green: #4A2; +$gl-text-red: #D12F19; +$gl-text-orange: #D90; +$gl-header-color: #4c4e54; +$gl-link-color: #333c48; +$md-text-color: #444; +$md-link-color: #3084bb; +$nprogress-color: #c0392b; +$gl-font-size: 15px; +$list-font-size: 15px; +$sidebar_collapsed_width: 62px; +$sidebar_width: 230px; +$avatar_radius: 50%; +$code_font_size: 13px; +$code_line_height: 1.5; +$border-color: #dce0e6; +$table-border-color: #eef0f2; +$background-color: #F7F8FA; +$header-height: 58px; +$fixed-layout-width: 1200px; +$gl-gray: #7f8fa4; +$gl-padding: 16px; +$gl-avatar-size: 46px; + +/* + * Color schema + */ + +$white-light: #FFFFFF; +$white-normal: #DCE0E5; +$white-dark: #E4E7ED; + +$gray-light: #F0F2F5; +$gray-normal: #DCE0E5; +$gray-dark: #E4E7ED; + +$green-light: #31AF64; +$green-normal: #2FAA60; +$green-dark: #2CA05B; + +$blue-light: #2EA8E5; +$blue-normal: #2D9FD8; +$blue-dark: #2897CE; + +$orange-light: #FC6443; +$orange-normal: #E75E40; +$orange-dark: #CE5237; + +$red-light: #F43263; +$red-normal: #E52C5A; +$red-dark: #D22852; + +$border-white-light: #E3E7EC; +$border-white-normal: #D6DAE2; +$border-white-dark: #C6CACF; + +$border-gray-light: #DCE0E5; +$border-gray-normal: #D6DAE2; +$border-gray-dark: #C6CACF; + +$border-green-light: #2FAA60; +$border-green-normal: #2CA05B; +$border-green-dark: #279654; + +$border-blue-light: #2D9FD8; +$border-blue-normal: #2897CE; +$border-blue-dark: #258DC1; + +$border-orange-light: #ED5C3D; +$border-orange-normal: #CE5237; +$border-orange-dark: #C14E35; + +$border-red-light: #E52C5A; +$border-red-normal: #D22852; +$border-red-dark: #CA264F; + + +/* + * State colors: + */ +$gl-primary: $blue-normal; +$gl-success: $green-normal; +$gl-info: $blue-normal; +$gl-warning: $orange-normal; +$gl-danger: $red-normal; + +/* + * Commit Diff Colors + */ +$added: #63c363; +$deleted: #f77; + +/* + * Fonts + */ +$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; +$regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif; diff --git a/app/assets/stylesheets/generic/zen.scss b/app/assets/stylesheets/framework/zen.scss similarity index 100% rename from app/assets/stylesheets/generic/zen.scss rename to app/assets/stylesheets/framework/zen.scss diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss deleted file mode 100644 index cf76f538e01..00000000000 --- a/app/assets/stylesheets/generic/buttons.scss +++ /dev/null @@ -1,228 +0,0 @@ -body { - text-rendering: geometricPrecision; -} -.btn { - @extend .btn-default; - - &.btn-new { - @extend .btn-success; - } - - &.btn-create { - @extend .btn-success; - } - - &.btn-save { - @extend .btn-success; - } - - &.btn-remove { - @extend .btn-danger; - } - - &.btn-cancel { - float: right; - } - - &.btn-close { - color: $gl-danger; - border-color: $gl-danger; - &:hover { - color: #B94A48; - } - } - - &.btn-reopen { - color: $gl-success; - border-color: $gl-success; - &:hover { - color: #468847; - } - } - - &.btn-grouped { - margin-right: 7px; - float: left; - &:last-child { - margin-right: 0px; - } - } - - &.btn-save { - @extend .btn-primary; - } - - &.btn-new, &.btn-create { - @extend .btn-success; - } -} - -.btn-block { - width: 100%; - margin: 0; - margin-bottom: 15px; - &.btn { - padding: 6px 0; - } -} - -.btn-group { - &.btn-grouped { - margin-right: 7px; - float: left; - &:last-child { - margin-right: 0px; - } - } -} - -.btn-group-next { - .btn { - padding: 9px 0px; - font-size: 15px; - color: #7f8fa4; - border-color: #e7e9ed; - width: 140px; - - &.active { - border-color: $gl-info; - background: $gl-info; - color: #fff; - } - } -} - -@mixin btn-info { - @include border-radius(2px); - - border-width: 1px; - border-style: solid; - text-transform: uppercase; - font-size: 13px; - font-weight: 600; - line-height: 18px; - padding: 11px 16px; - letter-spacing: .4px; - - &:hover { - border-width: 1px; - border-style: solid; - } - - &:focus { - border-width: 1px; - border-style: solid; - } - - &:active { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - border-width: 1px; - border-style: solid; - } -} - -@mixin btn-middle { - @include border-radius(2px); - - border-width: 1px; - border-style: solid; - text-transform: uppercase; - font-size: 13px; - font-weight: 600; - line-height: 18px; - padding: 11px 24px; - letter-spacing: .4px; - - &:hover { - border-width: 1px; - border-style: solid; - } - - &:focus { - border-width: 1px; - border-style: solid; - } - - &:active { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - border-width: 1px; - border-style: solid; - } -} - - -@mixin btn-green { - background-color: #28b061; - border: 1px solid #26a65c; - color: #fff; - - &:hover { - background-color: #26ab5d; - border: 1px solid #229954; - color: #fff; - } - - &:focus { - background-color: #26ab5d; - border: 1px solid #229954; - color: #fff; - } - - &:active { - @include box-shadow (inset 0 0 4px rgba(0, 0, 0, 0.12)); - - background-color: #23a158 !important; - border: 1px solid #229954 !important; - color: #fff !important; - } -} - -/*Butons*/ - -@mixin bnt-project { - background-color: #f0f2f5; - border-color: #dce0e5; - color: #313236; - - &:hover { - border-color:#dce0e5; - background-color: #ebeef2; - color: #313236; - } - - &:focus { - border-color: #dce0e5; - background-color: #ebeef2; - color: #313236; - } - - &:active { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - - color: #313236 !important; - border-color: #c6cacf !important; - background-color: #e4e7ed !important; - } -} - -@mixin btn-remove { - background-color: #f72e60; - border-color: #ee295a; - - &:hover { - background-color: #e82757; - border-color: #e32555; - } - - &:focus { - background-color: #e82757; - border-color: #e32555; - } - - &:active { - @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12)); - background-color: #d42450 !important; - border-color: #e12554 !important; - } - -} \ No newline at end of file diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss deleted file mode 100644 index 6a3cb49baae..00000000000 --- a/app/assets/stylesheets/generic/typography.scss +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Headers - * - */ -body { - text-rendering:optimizeLegibility; - -webkit-text-shadow: rgba(255,255,255,0.01) 0 0 1px; -} - -.page-title { - margin-top: 0px; - line-height: 1.3; - font-size: 1.25em; - font-weight: 600; -} - -.page-title-empty { - margin-top: 0px; - line-height: 1.3; - font-size: 1.25em; - font-weight: 600; - margin: 12px 7px 12px 7px; -} - -h1, h2, h3, h4, h5, h6 { - color: $gl-header-color; - font-weight: 500; -} - -/** CODE **/ -pre { - font-family: $monospace_font; - - &.dark { - background: #333; - color: $background-color; - } - - &.plain-readme { - background: none; - border: none; - padding: 0; - margin: 0; - font-size: 14px; - } -} - -.monospace { - font-family: $monospace_font; -} - -code { - &.key-fingerprint { - background: $body-bg; - color: $text-color; - } -} - -a > code { - color: $link-color; -} - -/** - * Wiki typography - * - */ -.wiki { - @include md-typography; - - word-wrap: break-word; - padding: 7px; - - /* Link to current header. */ - h1, h2, h3, h4, h5, h6 { - position: relative; - - a.anchor { - // Setting `display: none` would prevent the anchor being scrolled to, so - // instead we set the height to 0 and it gets updated on hover. - height: 0; - } - - &:hover > a.anchor { - $size: 16px; - position: absolute; - right: 100%; - top: 50%; - margin-top: -$size/2; - margin-right: 0px; - padding-right: 20px; - display: inline-block; - width: $size; - height: $size; - background-image: image-url("icon-link.png"); - background-size: contain; - background-repeat: no-repeat; - } - } - - ul,ol { - padding: 0; - margin: 6px 0 6px 18px !important; - } - ol { - color: #5c5d5e; - } -} - -.md-area { - @include md-typography; -} - -.md { - @include md-typography; -} - -/** - * Textareas intended for GFM - * - */ -textarea.js-gfm-input { - font-family: $monospace_font; -} - -.md-preview { -} - -.strikethrough { - text-decoration: line-through; -} \ No newline at end of file diff --git a/app/assets/stylesheets/ci/builds.scss b/app/assets/stylesheets/pages/builds.scss similarity index 99% rename from app/assets/stylesheets/ci/builds.scss rename to app/assets/stylesheets/pages/builds.scss index a27dd0db581..74dc3e321c1 100644 --- a/app/assets/stylesheets/ci/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -73,4 +73,3 @@ margin-bottom: 2px; } } - diff --git a/app/assets/stylesheets/ci/projects.scss b/app/assets/stylesheets/pages/ci_projects.scss similarity index 100% rename from app/assets/stylesheets/ci/projects.scss rename to app/assets/stylesheets/pages/ci_projects.scss diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index de2ae93df37..4e121b95d13 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -1,5 +1,6 @@ .commits-compare-switch{ - @extend .btn; + @include btn-default; + @include btn-white; background: image-url("switch_icon.png") no-repeat center center; text-indent: -9999px; float: left; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 5e7e59a6af8..d9ef06dc6b6 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -1,3 +1,4 @@ +// Common .diff-file { margin-left: -$gl-padding; margin-right: -$gl-padding; @@ -12,24 +13,17 @@ color: #555; z-index: 10; - > span { + .diff-title { font-family: $monospace_font; word-break: break-all; - margin-right: 200px; display: block; .file-mode { - margin-left: 10px; color: #777; } } - .diff-btn-group { - float: right; - position: absolute; - top: 5px; - right: 15px; - + .diff-controls { .btn { padding: 0px 10px; font-size: 13px; @@ -90,12 +84,12 @@ } } - tr.line_holder.parallel{ + tr.line_holder.parallel { .old_line, .new_line, .diff_line { min-width: 50px; } - td.line_content.parallel{ + td.line_content.parallel { width: 50%; } } @@ -105,7 +99,7 @@ padding: 0px; border: none; background: $background-color; - color: rgba(0,0,0,0.3); + color: rgba(0, 0, 0, 0.3); padding: 0px 5px; border-right: 1px solid $border-color; text-align: right; @@ -117,7 +111,7 @@ float: left; width: 35px; font-weight: normal; - color: rgba(0,0,0,0.3); + color: rgba(0, 0, 0, 0.3); &:hover { text-decoration: underline; } @@ -168,7 +162,7 @@ background: #ddd; text-align: center; padding: 30px; - .wrap{ + .wrap { display: inline-block; } @@ -176,7 +170,7 @@ display: inline-block; background-color: #fff; line-height: 0; - img{ + img { border: 1px solid #FFF; background: image-url('trans_bg.gif'); max-width: 100%; @@ -189,21 +183,21 @@ border: 1px solid $added; } } - .image-info{ + .image-info { font-size: 12px; margin: 5px 0 0 0; color: grey; } - .view.swipe{ + .view.swipe { position: relative; - .swipe-frame{ + .swipe-frame { display: block; margin: auto; position: relative; } - .swipe-wrap{ + .swipe-wrap { overflow: hidden; border-left: 1px solid #999; position: absolute; @@ -211,33 +205,33 @@ top: 13px; right: 7px; } - .frame{ + .frame { top: 0; right: 0; position: absolute; - &.deleted{ + &.deleted { margin: 0; display: block; top: 13px; right: 7px; } } - .swipe-bar{ + .swipe-bar { display: block; height: 100%; width: 15px; z-index: 100; position: absolute; cursor: pointer; - &:hover{ - .top-handle{ + &:hover { + .top-handle { background-position: -15px 3px; } - .bottom-handle{ + .bottom-handle { background-position: -15px -11px; } - }; - .top-handle{ + } + .top-handle { display: block; height: 14px; width: 15px; @@ -245,7 +239,7 @@ top: 0px; background: image-url('swipemode_sprites.gif') 0 3px no-repeat; } - .bottom-handle{ + .bottom-handle { display: block; height: 14px; width: 15px; @@ -254,9 +248,10 @@ background: image-url('swipemode_sprites.gif') 0 -11px no-repeat; } } - } //.view.swipe - .view.onion-skin{ - .onion-skin-frame{ + } + //.view.swipe + .view.onion-skin { + .onion-skin-frame { display: block; margin: auto; position: relative; @@ -267,7 +262,7 @@ top: 0px; left: 0px; } - .controls{ + .controls { display: block; height: 14px; width: 300px; @@ -277,7 +272,7 @@ left: 50%; margin-left: -150px; - .drag-track{ + .drag-track { display: block; position: absolute; left: 12px; @@ -317,39 +312,40 @@ background: image-url('onion_skin_sprites.gif') -2px -10px no-repeat; } } - } //.view.onion-skin + } + //.view.onion-skin } - .view-modes{ + .view-modes { padding: 10px; text-align: center; background: #EEE; - ul, li{ + ul, li { list-style: none; margin: 0; padding: 0; display: inline-block; } - li{ + li { color: grey; border-left: 1px solid #c1c1c1; padding: 0 12px 0 16px; cursor: pointer; - &:first-child{ + &:first-child { border-left: none; } - &:hover{ + &:hover { text-decoration: underline; } - &.active{ - &:hover{ + &.active { + &:hover { text-decoration: none; } cursor: default; color: #333; } - &.disabled{ + &.disabled { display: none; } } @@ -373,3 +369,37 @@ float: right; margin-top: -5px; } + +// Mobile +@media (max-width: 480px) { + .diff-title { + margin: 0; + + .file-mode { + display: none; + } + } + + .diff-controls { + position: static; + text-align: center; + } +} + +// Bigger screens +@media (min-width: 481px) { + .diff-title { + margin-right: 200px; + + .file-mode { + margin-left: 10px; + } + } + + .diff-controls { + float: right; + position: absolute; + top: 5px; + right: 15px; + } +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index b5c61f7f91d..9da085a3473 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -54,21 +54,22 @@ margin-top: -15px; padding: 10px 0; margin-bottom: 0; - color: $gl-gray; + color: #5c5d5e; font-size: 16px; .author { - color: $gl-gray; + color: #5c5d5e; } .issue-id { - font-size: 19px; - color: $gl-text-color; + color: #5c5d5e; } } .issue-title { margin: 0; + font-size: 23px; + color: #313236; } .description { diff --git a/app/assets/stylesheets/ci/lint.scss b/app/assets/stylesheets/pages/lint.scss similarity index 100% rename from app/assets/stylesheets/ci/lint.scss rename to app/assets/stylesheets/pages/lint.scss diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index d8c8e5ad0a4..a1a5208c59c 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -3,12 +3,11 @@ * */ .mr-state-widget { - background: #f8fafc; + background: #F7F8FA; margin-bottom: 20px; color: $gl-gray; - border: 1px solid #eef0f2; - @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.05)); - @include border-radius(3px); + border: 1px solid #dce0e6; + @include border-radius(2px); form { margin-bottom: 0; @@ -77,10 +76,16 @@ padding: 15px; } + .normal { + color: #5c5d5e; + } + .mr-widget-body { h4 { - font-weight: bold; + font-weight: 600; + font-size: 17px; margin: 5px 0; + color: #313236; } p:last-child { @@ -97,14 +102,26 @@ } } -.merge-request .merge-request-tabs{ +.merge-request .merge-request-tabs { @include nav-menu; margin: -$gl-padding; padding: $gl-padding; text-align: center; - border-top: 1px solid #e7e9ed; - margin-top: 18px; - margin-bottom: 3px; + margin-bottom: 1px; +} + +// Mobile +@media (max-width: 480px) { + .merge-request .merge-request-tabs { + margin: 0; + padding: 0; + + li { + a { + padding: 0; + } + } + } } .mr_source_commit, @@ -120,10 +137,12 @@ } .label-branch { - color: #222; + color: #313236; font-family: $monospace_font; font-weight: bold; overflow: hidden; + font-size: 14px; + margin: 0 3px; } .mr-list { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index fdc2c3332df..4392f08942b 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -65,19 +65,18 @@ .note-image-attach { @extend .col-md-4; - @extend .thumbnail; margin-left: 45px; float: none; } .common-note-form { margin: 0; - background: #f8fafc; + background: #F7F8FA; padding: $gl-padding; margin-left: -$gl-padding; margin-right: -$gl-padding; - border-right: 1px solid #f1f2f4; - border-top: 1px solid #f1f2f4; + border-right: 1px solid #ECEEF1; + border-top: 1px solid #ECEEF1; margin-bottom: -$gl-padding; } @@ -168,7 +167,7 @@ .comment-hints { color: #999; background: #FFF; - padding: 5px; + padding: 7px; margin-top: -11px; border: 1px solid $border-color; font-size: 13px; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 2a77f065aed..abb03b07f51 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -18,7 +18,7 @@ ul.notes { font-size: 14px; padding-top: 10px; padding-bottom: 10px; - background: #f8fafc; + background: #FDFDFD; .timeline-icon { .avatar { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 818aa10aefe..f7a22849003 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -13,11 +13,15 @@ .edit_project { fieldset.features { .control-label { - font-weight: bold; + font-weight: normal; } } } +.project-edit-content { + padding: 7px; +} + .project-name-holder { .help-inline { vertical-align: top; @@ -59,6 +63,7 @@ } p { + padding: 0 $gl-padding; color: #5c5d5e; } } @@ -92,8 +97,7 @@ margin-bottom: 0px; .btn { - @include bnt-project; - @include btn-info; + @include btn-gray; .count { display: inline-block; @@ -149,7 +153,7 @@ .input-group-btn { .btn { - @include bnt-project; + @include btn-gray; @include btn-middle; &:hover { @@ -183,8 +187,8 @@ margin: 0 12px 0 12px; .btn{ - @include bnt-project; - @include btn-info; + @include btn-gray; + @include btn-default; } .dropdown-toggle { @@ -251,18 +255,19 @@ margin-bottom: 10px; i { - margin: 0 3px; + margin: 2px 0; font-size: 20px; } .option-title { - font-weight: bold; + font-weight: normal; display: inline-block; + color: #313236; } .option-descr { - margin-left: 36px; - color: $gray; + margin-left: 29px; + color: #54565b; } } } @@ -376,8 +381,8 @@ table.table.protected-branches-list tr.no-border { } .nav > li > a { - @include btn-info; - @include bnt-project; + @include btn-default; + @include btn-gray; background-color: transparent; border: 1px solid #f7f8fa; @@ -437,7 +442,7 @@ pre.light-well { .btn-remove { @include btn-middle; - @include btn-remove; + @include btn-red; float: left !important; } @@ -506,8 +511,3 @@ pre.light-well { margin-top: -1px; } } - -.inline-form { - display: inline-block; -} - diff --git a/app/assets/stylesheets/ci/runners.scss b/app/assets/stylesheets/pages/runners.scss similarity index 100% rename from app/assets/stylesheets/ci/runners.scss rename to app/assets/stylesheets/pages/runners.scss diff --git a/app/assets/stylesheets/ci/status.scss b/app/assets/stylesheets/pages/status.scss similarity index 100% rename from app/assets/stylesheets/ci/status.scss rename to app/assets/stylesheets/pages/status.scss diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 271cc547e2b..dadd86e88cc 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,7 +1,7 @@ .tree-holder { - .tree-content-holder { - float: left; - width: 100%; + .tree-table-holder { + margin-left: -$gl-padding; + margin-right: -$gl-padding; } .tree_progress { @@ -13,10 +13,15 @@ } .tree-table { - @extend .table; - @include border-radius(0); + margin-bottom: 0; tr { + > td, > th { + padding: 10px $gl-padding; + line-height: 32px; + border-color: $table-border-color !important; + } + &:hover { td { background: $hover; @@ -27,9 +32,9 @@ } &.selected { td { - background: $background-color; - border-top: 1px solid #EEE; - border-bottom: 1px solid #EEE; + background: $gray-dark; + border-top: 1px solid $border-gray-dark; + border-bottom: 1px solid $border-gray-dark; } } } @@ -85,19 +90,6 @@ margin-right: 15px; } -.readme-holder { - margin: 0 auto; - - .readme-file-title { - font-size: 14px; - font-weight: bold; - margin-bottom: 20px; - color: #777; - border-bottom: 1px solid #DDD; - padding: 10px 0; - } -} - .blob-commit-info { list-style: none; margin: 0; diff --git a/app/assets/stylesheets/ci/xterm.scss b/app/assets/stylesheets/pages/xterm.scss similarity index 99% rename from app/assets/stylesheets/ci/xterm.scss rename to app/assets/stylesheets/pages/xterm.scss index 532dede0b23..9a50096c0d0 100644 --- a/app/assets/stylesheets/ci/xterm.scss +++ b/app/assets/stylesheets/pages/xterm.scss @@ -1,4 +1,4 @@ -.ci-body { +.build-page { // color codes are based on http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg // see also: https://gist.github.com/jasonm23/2868981 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 527c9da0faa..2b2ea3dff16 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -30,7 +30,7 @@ class ApplicationController < ActionController::Base rescue_from ActiveRecord::RecordNotFound do |exception| log_exception(exception) - render "errors/not_found", layout: "errors", status: 404 + render_404 end protected @@ -149,10 +149,6 @@ class ApplicationController < ActionController::Base render "errors/access_denied", layout: "errors", status: 404 end - def not_found! - render "errors/not_found", layout: "errors", status: 404 - end - def git_not_found! render "errors/git_not_found", layout: "errors", status: 404 end diff --git a/app/controllers/ci/builds_controller.rb b/app/controllers/ci/builds_controller.rb deleted file mode 100644 index b0b8b62fced..00000000000 --- a/app/controllers/ci/builds_controller.rb +++ /dev/null @@ -1,52 +0,0 @@ -module Ci - class BuildsController < Ci::ApplicationController - before_action :authenticate_user!, except: [:status] - before_action :project - before_action :authorize_access_project!, except: [:status] - before_action :authorize_manage_project!, except: [:status, :retry, :cancel] - before_action :authorize_manage_builds!, only: [:retry, :cancel] - before_action :build - - def retry - if @build.commands.blank? - return page_404 - end - - build = Ci::Build.retry(@build) - - if params[:return_to] - redirect_to URI.parse(params[:return_to]).path - else - redirect_to build_path(build) - end - end - - def status - render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha) - end - - def cancel - @build.cancel - - redirect_to build_path(@build) - end - - protected - - def project - @project = Ci::Project.find(params[:project_id]) - end - - def build - @build ||= project.builds.unscoped.find_by!(id: params[:id]) - end - - def commit_by_sha - @project.commits.find_by(sha: params[:id]) - end - - def build_path(build) - namespace_project_build_path(build.gl_project.namespace, build.gl_project, build) - end - end -end diff --git a/app/controllers/ci/commits_controller.rb b/app/controllers/ci/commits_controller.rb deleted file mode 100644 index 7e6705c9702..00000000000 --- a/app/controllers/ci/commits_controller.rb +++ /dev/null @@ -1,32 +0,0 @@ -module Ci - class CommitsController < Ci::ApplicationController - before_action :authenticate_user!, except: [:status, :show] - before_action :authenticate_public_page!, only: :show - before_action :project - before_action :authorize_access_project!, except: [:status, :show, :cancel] - before_action :authorize_manage_builds!, only: [:cancel] - - def status - commit = Ci::Project.find(params[:project_id]).commits.find_by_sha!(params[:id]) - render json: commit.to_json(only: [:id, :sha], methods: [:status, :coverage]) - rescue ActiveRecord::RecordNotFound - render json: { status: "not_found" } - end - - def cancel - commit.builds.running_or_pending.each(&:cancel) - - redirect_to namespace_project_commit_path(commit.gl_project.namespace, commit.gl_project, commit.sha) - end - - private - - def project - @project ||= Ci::Project.find(params[:project_id]) - end - - def commit - @commit ||= Ci::Project.find(params[:project_id]).commits.find_by_sha!(params[:id]) - end - end -end diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index adb913a783f..7777aa18031 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -1,27 +1,11 @@ module Ci class ProjectsController < Ci::ApplicationController - before_action :authenticate_user!, except: [:build, :badge, :show] - before_action :authenticate_public_page!, only: :show - before_action :project, only: [:build, :show, :badge, :toggle_shared_runners, :dumped_yaml] - before_action :authorize_access_project!, except: [:build, :badge, :show, :new] + before_action :project + before_action :authenticate_user!, except: [:build, :badge] + before_action :authorize_access_project!, except: [:badge] before_action :authorize_manage_project!, only: [:toggle_shared_runners, :dumped_yaml] - before_action :authenticate_token!, only: [:build] before_action :no_cache, only: [:badge] - protect_from_forgery except: :build - - layout 'ci/project', except: [:index] - - def show - @ref = params[:ref] - - @commits = @project.commits.reverse_order - if @ref - # unscope is required, because of default_scope defined in Ci::Build - builds = @project.builds.unscope(:select, :order).where(ref: @ref).select(:commit_id).distinct - @commits = @commits.where(id: builds) - end - @commits = @commits.page(params[:page]).per(20) - end + protect_from_forgery # Project status badge # Image with build status for sha or ref diff --git a/app/controllers/ci/services_controller.rb b/app/controllers/ci/services_controller.rb deleted file mode 100644 index 52c96a34ce8..00000000000 --- a/app/controllers/ci/services_controller.rb +++ /dev/null @@ -1,59 +0,0 @@ -module Ci - class ServicesController < Ci::ApplicationController - before_action :authenticate_user! - before_action :project - before_action :authorize_access_project! - before_action :authorize_manage_project! - before_action :service, only: [:edit, :update, :test] - - respond_to :html - - layout 'ci/project' - - def index - @project.build_missing_services - @services = @project.services.reload - end - - def edit - end - - def update - if @service.update_attributes(service_params) - redirect_to edit_ci_project_service_path(@project, @service.to_param) - else - render 'edit' - end - end - - def test - last_build = @project.builds.last - - if @service.execute(last_build) - message = { notice: 'We successfully tested the service' } - else - message = { alert: 'We tried to test the service but error occurred' } - end - - redirect_to :back, message - end - - private - - def project - @project = Ci::Project.find(params[:project_id]) - end - - def service - @service ||= @project.services.find { |service| service.to_param == params[:id] } - end - - def service_params - params.require(:service).permit( - :type, :active, :webhook, :notify_only_broken_builds, - :email_recipients, :email_only_broken_builds, :email_add_pusher, - :hipchat_token, :hipchat_room, :hipchat_server - ) - end - end -end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 524218290c6..40fb15a5b36 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -88,7 +88,7 @@ class GroupsController < Groups::ApplicationController def destroy DestroyGroupService.new(@group, current_user).execute - redirect_to root_path, alert: "Group '#{@group.name} was deleted." + redirect_to root_path, alert: "Group '#{@group.name}' was successfully deleted." end protected diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index f84f85a7df8..25e58724860 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -62,7 +62,7 @@ class Import::BitbucketController < Import::BaseController end def verify_bitbucket_import_enabled - not_found! unless bitbucket_import_enabled? + render_404 unless bitbucket_import_enabled? end def bitbucket_auth diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb index 849646cd665..18300390851 100644 --- a/app/controllers/import/fogbugz_controller.rb +++ b/app/controllers/import/fogbugz_controller.rb @@ -99,6 +99,6 @@ class Import::FogbugzController < Import::BaseController end def verify_fogbugz_import_enabled - not_found! unless fogbugz_import_enabled? + render_404 unless fogbugz_import_enabled? end end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index f21fbd9ecca..aae77d384c6 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -47,7 +47,7 @@ class Import::GithubController < Import::BaseController end def verify_github_import_enabled - not_found! unless github_import_enabled? + render_404 unless github_import_enabled? end def github_auth diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index 27af19f5f61..23a396e8084 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -44,7 +44,7 @@ class Import::GitlabController < Import::BaseController end def verify_gitlab_import_enabled - not_found! unless gitlab_import_enabled? + render_404 unless gitlab_import_enabled? end def gitlab_auth diff --git a/app/controllers/import/gitorious_controller.rb b/app/controllers/import/gitorious_controller.rb index f24cdb3709a..eecbe380c9e 100644 --- a/app/controllers/import/gitorious_controller.rb +++ b/app/controllers/import/gitorious_controller.rb @@ -42,7 +42,7 @@ class Import::GitoriousController < Import::BaseController end def verify_gitorious_import_enabled - not_found! unless gitorious_import_enabled? + render_404 unless gitorious_import_enabled? end end diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb index 82fadeb7e83..41472a6fe6c 100644 --- a/app/controllers/import/google_code_controller.rb +++ b/app/controllers/import/google_code_controller.rb @@ -106,7 +106,7 @@ class Import::GoogleCodeController < Import::BaseController end def verify_google_code_import_enabled - not_found! unless google_code_import_enabled? + render_404 unless google_code_import_enabled? end def user_map diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index 9c3763d5934..548f1b9ebfe 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -12,7 +12,7 @@ class Projects::AvatarsController < Projects::ApplicationController filename: @blob.name ) else - not_found! + render_404 end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 8776721d243..8cc2f21d887 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -8,7 +8,7 @@ class Projects::BlobController < Projects::ApplicationController before_action :require_non_empty_project, except: [:new, :create] before_action :authorize_download_code! - before_action :authorize_push_code!, only: [:destroy] + before_action :authorize_push_code!, only: [:destroy, :create] before_action :assign_blob_vars before_action :commit, except: [:new, :create] before_action :blob, except: [:new, :create] @@ -25,7 +25,7 @@ class Projects::BlobController < Projects::ApplicationController result = Files::CreateService.new(@project, current_user, @commit_params).execute if result[:status] == :success - flash[:notice] = "Your changes have been successfully committed" + flash[:notice] = "The changes have been successfully committed" respond_to do |format| format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) } format.json { render json: { message: "success", filePath: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) } } @@ -34,7 +34,7 @@ class Projects::BlobController < Projects::ApplicationController flash[:alert] = result[:message] respond_to do |format| format.html { render :new } - format.json { render json: { message: "failed", filePath: namespace_project_new_blob_path(@project.namespace, @project, @id) } } + format.json { render json: { message: "failed", filePath: namespace_project_blob_path(@project.namespace, @project, @id) } } end end end @@ -113,14 +113,14 @@ class Projects::BlobController < Projects::ApplicationController end end - return not_found! + return render_404 end end def commit @commit = @repository.commit(@ref) - return not_found! unless @commit + return render_404 unless @commit end def assign_blob_vars @@ -128,7 +128,7 @@ class Projects::BlobController < Projects::ApplicationController @ref, @path = extract_ref(@id) rescue InvalidPathError - not_found! + render_404 end def after_edit_path @@ -154,7 +154,7 @@ class Projects::BlobController < Projects::ApplicationController def editor_variables @current_branch = @ref - @target_branch = (sanitized_new_branch_name || @ref) + @target_branch = params[:new_branch].present? ? sanitized_new_branch_name : @ref @file_path = if action_name.to_s == 'create' diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 76c7f31f61b..4e4ac6689d3 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -2,6 +2,8 @@ class Projects::BuildsController < Projects::ApplicationController before_action :ci_project before_action :build + before_action :authorize_admin_project!, except: [:show, :status] + layout "project" def show @@ -17,9 +19,37 @@ class Projects::BuildsController < Projects::ApplicationController end end + def retry + if @build.commands.blank? + return page_404 + end + + build = Ci::Build.retry(@build) + + if params[:return_to] + redirect_to URI.parse(params[:return_to]).path + else + redirect_to build_path(build) + end + end + + def status + render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha) + end + + def cancel + @build.cancel + + redirect_to build_path(@build) + end + private def build @build ||= ci_project.builds.unscoped.find_by!(id: params[:id]) end + + def build_path(build) + namespace_project_build_path(build.gl_project.namespace, build.gl_project, build) + end end diff --git a/app/controllers/projects/ci_services_controller.rb b/app/controllers/projects/ci_services_controller.rb new file mode 100644 index 00000000000..6d2756eba3d --- /dev/null +++ b/app/controllers/projects/ci_services_controller.rb @@ -0,0 +1,49 @@ +class Projects::CiServicesController < Projects::ApplicationController + before_action :ci_project + before_action :authorize_admin_project! + + layout "project_settings" + + def index + @ci_project.build_missing_services + @services = @ci_project.services.reload + end + + def edit + service + end + + def update + if @service.update_attributes(service_params) + redirect_to edit_namespace_project_ci_service_path(@project, @project.namespace, @service.to_param) + else + render 'edit' + end + end + + def test + last_build = @project.builds.last + + if @service.execute(last_build) + message = { notice: 'We successfully tested the service' } + else + message = { alert: 'We tried to test the service but error occurred' } + end + + redirect_to :back, message + end + + private + + def service + @service ||= @ci_project.services.find { |service| service.to_param == params[:id] } + end + + def service_params + params.require(:service).permit( + :type, :active, :webhook, :notify_only_broken_builds, + :email_recipients, :email_only_broken_builds, :email_add_pusher, + :hipchat_token, :hipchat_room, :hipchat_server + ) + end +end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 1938c63c10c..7886f3c6deb 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -38,6 +38,14 @@ class Projects::CommitController < Projects::ApplicationController @ci_project = @project.gitlab_ci_project end + def cancel_builds + @ci_commit = @project.ci_commit(@commit.sha) + @ci_commit.builds.running_or_pending.each(&:cancel) + + redirect_to ci_namespace_project_commit_path(project.namespace, project, commit.sha) + end + + def branches @branches = @project.repository.branch_names_contains(commit.id) @tags = @project.repository.tag_names_contains(commit.id) diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index 5f6fbce795e..d5ee6ac8663 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -20,7 +20,7 @@ class Projects::RawController < Projects::ApplicationController disposition: 'inline' ) else - not_found! + render_404 end end diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index 92e4bc16d9d..bdcb1a3e297 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -1,13 +1,16 @@ # Controller for viewing a repository's file structure class Projects::TreeController < Projects::ApplicationController include ExtractsPath + include ActionView::Helpers::SanitizeHelper before_action :require_non_empty_project, except: [:new, :create] before_action :assign_ref_vars + before_action :assign_dir_vars, only: [:create_dir] before_action :authorize_download_code! + before_action :authorize_push_code!, only: [:create_dir] def show - return not_found! unless @repository.commit(@ref) + return render_404 unless @repository.commit(@ref) if tree.entries.empty? if @repository.blob_at(@commit.id, @path) @@ -16,7 +19,7 @@ class Projects::TreeController < Projects::ApplicationController File.join(@ref, @path)) ) and return elsif @path.present? - return not_found! + return render_404 end end @@ -26,4 +29,38 @@ class Projects::TreeController < Projects::ApplicationController format.js { no_cache_headers } end end + + def create_dir + return render_404 unless @commit_params.values.all? + + begin + result = Files::CreateDirService.new(@project, current_user, @commit_params).execute + message = result[:message] + rescue => e + message = e.to_s + end + + if result && result[:status] == :success + flash[:notice] = "The directory has been successfully created" + respond_to do |format| + format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @dir_name)) } + end + else + flash[:alert] = message + respond_to do |format| + format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, @new_branch) } + end + end + end + + def assign_dir_vars + @new_branch = params[:new_branch].present? ? sanitize(strip_tags(params[:new_branch])) : @ref + @dir_name = File.join(@path, params[:dir_name]) + @commit_params = { + file_path: @dir_name, + current_branch: @ref, + target_branch: @new_branch, + commit_message: params[:commit_message], + } + end end diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb index 71ecc20dd95..e1fe7ea2114 100644 --- a/app/controllers/projects/uploads_controller.rb +++ b/app/controllers/projects/uploads_controller.rb @@ -20,7 +20,7 @@ class Projects::UploadsController < Projects::ApplicationController end def show - return not_found! if uploader.nil? || !uploader.file.exists? + return render_404 if uploader.nil? || !uploader.file.exists? disposition = uploader.image? ? 'inline' : 'attachment' send_file uploader.file.path, disposition: disposition diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 28536e359e5..868b05929d7 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -10,7 +10,7 @@ class UploadsController < ApplicationController end unless uploader.file && uploader.file.exists? - return not_found! + return render_404 end disposition = uploader.image? ? 'inline' : 'attachment' @@ -21,7 +21,7 @@ class UploadsController < ApplicationController def find_model unless upload_model && upload_mount - return not_found! + return render_404 end @model = upload_model.find(params[:id]) @@ -44,7 +44,7 @@ class UploadsController < ApplicationController return if authorized if current_user - not_found! + render_404 else authenticate_user! end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 6aa16673d63..97c7e74c294 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -72,11 +72,15 @@ class IssuableFinder params[:milestone_title].present? end + def no_milestones? + milestones? && params[:milestone_title] == Milestone::None.title + end + def milestones return @milestones if defined?(@milestones) @milestones = - if milestones? && params[:milestone_title] != Milestone::None.title + if milestones? Milestone.where(title: params[:milestone_title]) else nil @@ -183,7 +187,11 @@ class IssuableFinder def by_milestone(items) if milestones? - items = items.where(milestone_id: milestones.try(:pluck, :id)) + if no_milestones? + items = items.where(milestone_id: [-1, nil]) + else + items = items.where(milestone_id: milestones.try(:pluck, :id)) + end end items @@ -207,13 +215,19 @@ class IssuableFinder def by_label(items) if params[:label_name].present? - label_names = params[:label_name].split(",") + if params[:label_name] == Label::None.title + item_ids = LabelLink.where(target_type: klass.name).pluck(:target_id) - item_ids = LabelLink.joins(:label). - where('labels.title in (?)', label_names). - where(target_type: klass.name).pluck(:target_id) + items = items.where('id NOT IN (?)', item_ids) + else + label_names = params[:label_name].split(",") - items = items.where(id: item_ids) + item_ids = LabelLink.joins(:label). + where('labels.title in (?)', label_names). + where(target_type: klass.name).pluck(:target_id) + + items = items.where(id: item_ids) + end end items diff --git a/app/finders/trending_projects_finder.rb b/app/finders/trending_projects_finder.rb index 9ea342cb26d..81a12403801 100644 --- a/app/finders/trending_projects_finder.rb +++ b/app/finders/trending_projects_finder.rb @@ -1,13 +1,6 @@ class TrendingProjectsFinder - def execute(current_user, start_date = nil) - start_date ||= Date.today - 1.month - - projects = projects_for(current_user) - - # Determine trending projects based on comments count - # for period of time - ex. month - projects.joins(:notes).where('notes.created_at > ?', start_date). - group("projects.id").reorder("count(notes.id) DESC") + def execute(current_user, start_date = 1.month.ago) + projects_for(current_user).trending(start_date) end private diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 8036303851b..66b18eea699 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -93,7 +93,10 @@ module LabelsHelper end def project_labels_options(project) - options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name]) + labels = project.labels.to_a + labels.unshift(Label::None) + labels.unshift(Label::Any) + options_from_collection_for_select(labels, 'name', 'title', params[:label_name]) end # Required for Gitlab::Markdown::LabelReferenceFilter diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 132a893e532..37a5b58cce8 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -30,7 +30,8 @@ module MilestonesHelper grouped_milestones = Milestones::GroupService.new(milestones).execute grouped_milestones.unshift(Milestone::None) + grouped_milestones.unshift(Milestone::Any) - options_from_collection_for_select(grouped_milestones, 'title', 'title', params[:milestone_title]) + options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title]) end end diff --git a/app/models/ability.rb b/app/models/ability.rb index a020b24a550..77c121ca5e8 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -135,6 +135,8 @@ class Ability def project_report_rules project_guest_rules + [ + :create_commit_status, + :read_commit_statuses, :download_code, :fork_project, :create_project_snippet, diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5d17f4418ed..cfafbf6786e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -24,32 +24,19 @@ # module Ci - class Build < ActiveRecord::Base - extend Ci::Model - + class Build < CommitStatus LAZY_ATTRIBUTES = ['trace'] - belongs_to :commit, class_name: 'Ci::Commit' belongs_to :runner, class_name: 'Ci::Runner' belongs_to :trigger_request, class_name: 'Ci::TriggerRequest' - belongs_to :user serialize :options - validates :commit, presence: true - validates :status, presence: true validates :coverage, numericality: true, allow_blank: true validates_presence_of :ref - scope :running, ->() { where(status: "running") } - scope :pending, ->() { where(status: "pending") } - scope :success, ->() { where(status: "success") } - scope :failed, ->() { where(status: "failed") } scope :unstarted, ->() { where(runner_id: nil) } - scope :running_or_pending, ->() { where(status:[:running, :pending]) } - scope :latest, ->() { where(id: unscope(:select).select('max(id)').group(:name, :ref)).order(stage_idx: :asc) } scope :ignore_failures, ->() { where(allow_failure: false) } - scope :for_ref, ->(ref) { where(ref: ref) } scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) } acts_as_taggable @@ -74,13 +61,14 @@ module Ci def create_from(build) new_build = build.dup - new_build.status = :pending + new_build.status = 'pending' new_build.runner_id = nil + new_build.trigger_request_id = nil new_build.save end def retry(build) - new_build = Ci::Build.new(status: :pending) + new_build = Ci::Build.new(status: 'pending') new_build.ref = build.ref new_build.tag = build.tag new_build.options = build.options @@ -98,28 +86,7 @@ module Ci end state_machine :status, initial: :pending do - event :run do - transition pending: :running - end - - event :drop do - transition running: :failed - end - - event :success do - transition running: :success - end - - event :cancel do - transition [:pending, :running] => :canceled - end - - after_transition pending: :running do |build, transition| - build.update_attributes started_at: Time.now - end - after_transition any => [:success, :failed, :canceled] do |build, transition| - build.update_attributes finished_at: Time.now project = build.project if project.web_hooks? @@ -136,19 +103,10 @@ module Ci build.update_coverage end end - - state :pending, value: 'pending' - state :running, value: 'running' - state :failed, value: 'failed' - state :success, value: 'success' - state :canceled, value: 'canceled' end - delegate :sha, :short_sha, :project, :gl_project, - to: :commit, prefix: false - - def before_sha - Gitlab::Git::BLANK_SHA + def ignored? + failed? && allow_failure? end def trace_html @@ -156,22 +114,6 @@ module Ci html || '' end - def started? - !pending? && !canceled? && started_at - end - - def active? - running? || pending? - end - - def complete? - canceled? || success? || failed? - end - - def ignored? - failed? && allow_failure? - end - def timeout project.timeout end @@ -180,14 +122,6 @@ module Ci yaml_variables + project_variables + trigger_variables end - def duration - if started_at && finished_at - finished_at - started_at - elsif started_at - Time.now - started_at - end - end - def project commit.project end @@ -278,6 +212,25 @@ module Ci "#{dir_to_trace}/#{id}.log" end + def target_url + Gitlab::Application.routes.url_helpers. + namespace_project_build_url(gl_project.namespace, gl_project, self) + end + + def cancel_url + if active? + Gitlab::Application.routes.url_helpers. + cancel_namespace_project_build_path(gl_project.namespace, gl_project, self) + end + end + + def retry_url + if commands.present? + Gitlab::Application.routes.url_helpers. + retry_namespace_project_build_path(gl_project.namespace, gl_project, self) + end + end + private def yaml_variables diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index fde754a92a1..68864edfbbf 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -20,7 +20,8 @@ module Ci extend Ci::Model belongs_to :gl_project, class_name: '::Project', foreign_key: :gl_project_id - has_many :builds, dependent: :destroy, class_name: 'Ci::Build' + has_many :statuses, dependent: :destroy, class_name: 'CommitStatus' + has_many :builds, class_name: 'Ci::Build' has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' validates_presence_of :sha @@ -47,7 +48,7 @@ module Ci end def retry - builds_without_retry.each do |build| + latest_builds.each do |build| Ci::Build.retry(build) end end @@ -81,12 +82,11 @@ module Ci end def stage - running_or_pending = builds_without_retry.running_or_pending - running_or_pending.limit(1).pluck(:stage).first + running_or_pending = statuses.latest.running_or_pending.ordered + running_or_pending.first.try(:stage) end def create_builds(ref, tag, user, trigger_request = nil) - return if skip_ci? && trigger_request.blank? return unless config_processor config_processor.stages.any? do |stage| CreateBuildsService.new.execute(self, stage, ref, tag, user, trigger_request).present? @@ -94,7 +94,6 @@ module Ci end def create_next_builds(ref, tag, user, trigger_request) - return if skip_ci? && trigger_request.blank? return unless config_processor stages = builds.where(ref: ref, tag: tag, trigger_request: trigger_request).group_by(&:stage) @@ -107,61 +106,60 @@ module Ci end def refs - builds.group(:ref).pluck(:ref) + statuses.order(:ref).pluck(:ref).uniq end - def last_ref - builds.latest.first.try(:ref) + def latest_statuses + @latest_statuses ||= statuses.latest.to_a end - def builds_without_retry - builds.latest + def latest_builds + @latest_builds ||= builds.latest.to_a end - def builds_without_retry_for_ref(ref) - builds.for_ref(ref).latest + def latest_builds_for_ref(ref) + latest_builds.select { |build| build.ref == ref } end - def retried_builds - @retried_builds ||= (builds.order(id: :desc) - builds_without_retry) + def retried + @retried ||= (statuses.order(id: :desc) - statuses.latest) end def status - if skip_ci? - return 'skipped' - elsif yaml_errors.present? + if yaml_errors.present? return 'failed' - elsif builds.none? - return 'skipped' - elsif success? - 'success' - elsif pending? - 'pending' - elsif running? - 'running' - elsif canceled? - 'canceled' - else - 'failed' + end + + @status ||= begin + latest = latest_statuses + latest.reject! { |status| status.try(&:allow_failure?) } + + if latest.none? + 'skipped' + elsif latest.all?(&:success?) + 'success' + elsif latest.all?(&:pending?) + 'pending' + elsif latest.any?(&:running?) || latest.any?(&:pending?) + 'running' + elsif latest.all?(&:canceled?) + 'canceled' + else + 'failed' + end end end def pending? - builds_without_retry.all? do |build| - build.pending? - end + status == 'pending' end def running? - builds_without_retry.any? do |build| - build.running? || build.pending? - end + status == 'running' end def success? - builds_without_retry.all? do |build| - build.success? || build.ignored? - end + status == 'success' end def failed? @@ -169,26 +167,21 @@ module Ci end def canceled? - builds_without_retry.all? do |build| - build.canceled? - end + status == 'canceled' end def duration - @duration ||= builds_without_retry.select(&:duration).sum(&:duration).to_i - end - - def duration_for_ref(ref) - builds_without_retry_for_ref(ref).select(&:duration).sum(&:duration).to_i + duration_array = latest_statuses.map(&:duration).compact + duration_array.reduce(:+).to_i end def finished_at - @finished_at ||= builds.order('finished_at DESC').first.try(:finished_at) + @finished_at ||= statuses.order('finished_at DESC').first.try(:finished_at) end def coverage if project.coverage_enabled? - coverage_array = builds_without_retry.map(&:coverage).compact + coverage_array = latest_builds.map(&:coverage).compact if coverage_array.size >= 1 '%.2f' % (coverage_array.reduce(:+) / coverage_array.size) end @@ -196,7 +189,7 @@ module Ci end def matrix_for_ref?(ref) - builds_without_retry_for_ref(ref).pluck(:id).size > 1 + latest_builds_for_ref(ref).size > 1 end def config_processor @@ -217,7 +210,6 @@ module Ci end def skip_ci? - return false if builds.any? git_commit_message =~ /(\[ci skip\])/ if git_commit_message end diff --git a/app/models/commit.rb b/app/models/commit.rb index 95ac7156bed..23b5e38336c 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -184,4 +184,12 @@ class Commit def parents @parents ||= Commit.decorate(super, project) end + + def ci_commit + project.ci_commit(sha) + end + + def status + ci_commit.try(:status) || :not_found + end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb new file mode 100644 index 00000000000..b4d91b1b0c3 --- /dev/null +++ b/app/models/commit_status.rb @@ -0,0 +1,91 @@ +class CommitStatus < ActiveRecord::Base + self.table_name = 'ci_builds' + + belongs_to :commit, class_name: 'Ci::Commit' + belongs_to :user + + validates :commit, presence: true + validates :status, inclusion: { in: %w(pending running failed success canceled) } + + validates_presence_of :name + + alias_attribute :author, :user + + scope :running, -> { where(status: 'running') } + scope :pending, -> { where(status: 'pending') } + scope :success, -> { where(status: 'success') } + scope :failed, -> { where(status: 'failed') } + scope :running_or_pending, -> { where(status:[:running, :pending]) } + scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)) } + scope :ordered, -> { order(:ref, :stage_idx, :name) } + scope :for_ref, ->(ref) { where(ref: ref) } + scope :running_or_pending, -> { where(status: [:running, :pending]) } + + state_machine :status, initial: :pending do + event :run do + transition pending: :running + end + + event :drop do + transition running: :failed + end + + event :success do + transition [:pending, :running] => :success + end + + event :cancel do + transition [:pending, :running] => :canceled + end + + after_transition pending: :running do |build, transition| + build.update_attributes started_at: Time.now + end + + after_transition any => [:success, :failed, :canceled] do |build, transition| + build.update_attributes finished_at: Time.now + end + + state :pending, value: 'pending' + state :running, value: 'running' + state :failed, value: 'failed' + state :success, value: 'success' + state :canceled, value: 'canceled' + end + + delegate :sha, :short_sha, :gl_project, + to: :commit, prefix: false + + # TODO: this should be removed with all references + def before_sha + Gitlab::Git::BLANK_SHA + end + + def started? + !pending? && !canceled? && started_at + end + + def active? + running? || pending? + end + + def complete? + canceled? || success? || failed? + end + + def duration + if started_at && finished_at + finished_at - started_at + elsif started_at + Time.now - started_at + end + end + + def cancel_url + nil + end + + def retry_url + nil + end +end diff --git a/app/models/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb new file mode 100644 index 00000000000..fe0cea8465f --- /dev/null +++ b/app/models/concerns/case_sensitivity.rb @@ -0,0 +1,28 @@ +# Concern for querying columns with specific case sensitivity handling. +module CaseSensitivity + extend ActiveSupport::Concern + + module ClassMethods + # Queries the given columns regardless of the casing used. + # + # Unlike other ActiveRecord methods this method only operates on a Hash. + def iwhere(params) + criteria = self + cast_lower = Gitlab::Database.postgresql? + + params.each do |key, value| + column = ActiveRecord::Base.connection.quote_table_name(key) + + if cast_lower + condition = "LOWER(#{column}) = LOWER(:value)" + else + condition = "#{column} = :value" + end + + criteria = criteria.where(condition, value: value) + end + + criteria + end + end +end diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb new file mode 100644 index 00000000000..fa54e3540d0 --- /dev/null +++ b/app/models/generic_commit_status.rb @@ -0,0 +1,15 @@ +class GenericCommitStatus < CommitStatus + before_validation :set_default_values + + # GitHub compatible API + alias_attribute :context, :name + + def set_default_values + self.context ||= 'default' + self.stage ||= 'external' + end + + def tags + [:external] + end +end diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb index ab055f6b80b..1dd2be68ebf 100644 --- a/app/models/group_milestone.rb +++ b/app/models/group_milestone.rb @@ -1,5 +1,7 @@ class GroupMilestone + alias_attribute :name, :title + def initialize(title, milestones) @title = title @milestones = milestones diff --git a/app/models/label.rb b/app/models/label.rb index 4a22bd53400..1bb4b5f55cf 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -12,6 +12,11 @@ class Label < ActiveRecord::Base include Referable + # Represents a "No Label" state used for filtering Issues and Merge + # Requests that have no label assigned. + LabelStruct = Struct.new(:title, :name) + None = LabelStruct.new('No Label', 'No Label') + Any = LabelStruct.new('Any', '') DEFAULT_COLOR = '#428BCA' diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index eb468c6cd53..c83b15c7d39 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -227,7 +227,7 @@ class MergeRequest < ActiveRecord::Base end def work_in_progress? - title =~ /\A\[?WIP\]?:? /i + !!(title =~ /\A\[?WIP\]?:? /i) end def mergeable? @@ -275,7 +275,8 @@ class MergeRequest < ActiveRecord::Base attrs = { source: source_project.hook_attrs, target: target_project.hook_attrs, - last_commit: nil + last_commit: nil, + work_in_progress: work_in_progress? } unless last_commit.nil? diff --git a/app/models/milestone.rb b/app/models/milestone.rb index d979a35084b..84acba30b6b 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -16,7 +16,9 @@ class Milestone < ActiveRecord::Base # Represents a "No Milestone" state used for filtering Issues and Merge # Requests that have no milestone assigned. - None = Struct.new(:title).new('No Milestone') + MilestoneStruct = Struct.new(:title, :name) + None = MilestoneStruct.new('No Milestone', 'No Milestone') + Any = MilestoneStruct.new('Any', '') include InternalId include Sortable @@ -47,6 +49,8 @@ class Milestone < ActiveRecord::Base state :active end + alias_attribute :name, :title + class << self def search(query) query = "%#{query}%" diff --git a/app/models/namespace.rb b/app/models/namespace.rb index bc8525df5a5..5782e649f8b 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -118,6 +118,8 @@ class Namespace < ActiveRecord::Base gitlab_shell.add_namespace(path_was) if gitlab_shell.mv_namespace(path_was, path) + Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) + # If repositories moved successfully we need to # send update instructions to users. # However we cannot allow rollback since we moved namespace dir diff --git a/app/models/project.rb b/app/models/project.rb index bb47b9abb03..cd30467fae3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -40,6 +40,7 @@ class Project < ActiveRecord::Base include Referable include Sortable include AfterCommitQueue + include CaseSensitivity extend Gitlab::ConfigHelper extend Enumerize @@ -235,13 +236,18 @@ class Project < ActiveRecord::Base end def find_with_namespace(id) - return nil unless id.include?('/') + namespace_path, project_path = id.split('/') - id = id.split('/') - namespace = Namespace.by_path(id.first) - return nil unless namespace + return nil if !namespace_path || !project_path - where(namespace_id: namespace.id).where("LOWER(projects.path) = :path", path: id.second.downcase).first + # Use of unscoped ensures we're not secretly adding any ORDER BYs, which + # have a negative impact on performance (and aren't needed for this + # query). + unscoped. + joins(:namespace). + iwhere('namespaces.path' => namespace_path). + iwhere('projects.path' => project_path). + take end def visibility_levels @@ -260,6 +266,20 @@ class Project < ActiveRecord::Base name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR %r{(?#{name_pattern}/#{name_pattern})} end + + def trending(since = 1.month.ago) + # By counting in the JOIN we don't expose the GROUP BY to the outer query. + # This means that calls such as "any?" and "count" just return a number of + # the total count, instead of the counts grouped per project as a Hash. + join_body = "INNER JOIN ( + SELECT project_id, COUNT(*) AS amount + FROM notes + WHERE created_at >= #{sanitize(since)} + GROUP BY project_id + ) join_note_counts ON projects.id = join_note_counts.project_id" + + joins(join_body).reorder('join_note_counts.amount DESC') + end end def team @@ -636,6 +656,8 @@ class Project < ActiveRecord::Base # db changes in order to prevent out of sync between db and fs raise Exception.new('repository cannot be renamed') end + + Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.path) end def hook_attrs diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index d8aedbd2ab4..5f5255ab487 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -40,12 +40,19 @@ class BambooService < CiService attr_accessor :response after_save :compose_service_hook, if: :activated? + before_update :reset_password def compose_service_hook hook = service_hook || build_service_hook hook.save end + def reset_password + if prop_updated?(:bamboo_url) + self.password = nil + end + end + def title 'Atlassian Bamboo CI' end diff --git a/app/models/project_services/ci/hip_chat_service.rb b/app/models/project_services/ci/hip_chat_service.rb index 0e6e97394bc..f17993d9f3b 100644 --- a/app/models/project_services/ci/hip_chat_service.rb +++ b/app/models/project_services/ci/hip_chat_service.rb @@ -49,7 +49,7 @@ module Ci commit = build.commit return unless commit - return unless commit.builds_without_retry.include? build + return unless commit.latest_builds.include? build case commit.status.to_sym when :failed diff --git a/app/models/project_services/ci/mail_service.rb b/app/models/project_services/ci/mail_service.rb index 11a2743f969..fd193301001 100644 --- a/app/models/project_services/ci/mail_service.rb +++ b/app/models/project_services/ci/mail_service.rb @@ -48,7 +48,7 @@ module Ci # it doesn't make sense to send emails for retried builds commit = build.commit return unless commit - return unless commit.builds_without_retry.include?(build) + return unless commit.latest_builds.include?(build) case build.status.to_sym when :failed diff --git a/app/models/project_services/ci/slack_message.rb b/app/models/project_services/ci/slack_message.rb index 5ac8907ecd0..dc050a3fc59 100644 --- a/app/models/project_services/ci/slack_message.rb +++ b/app/models/project_services/ci/slack_message.rb @@ -23,7 +23,7 @@ module Ci def attachments fields = [] - commit.builds_without_retry.each do |build| + commit.latest_builds.each do |build| next if build.allow_failure? next unless build.failed? fields << { diff --git a/app/models/project_services/ci/slack_service.rb b/app/models/project_services/ci/slack_service.rb index 76db573dc17..ee8e4988826 100644 --- a/app/models/project_services/ci/slack_service.rb +++ b/app/models/project_services/ci/slack_service.rb @@ -48,7 +48,7 @@ module Ci commit = build.commit return unless commit - return unless commit.builds_without_retry.include?(build) + return unless commit.latest_builds.include?(build) case commit.status.to_sym when :failed diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 3c002a1634b..fb11cad352e 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -37,12 +37,19 @@ class TeamcityService < CiService attr_accessor :response after_save :compose_service_hook, if: :activated? + before_update :reset_password def compose_service_hook hook = service_hook || build_service_hook hook.save end + def reset_password + if prop_updated?(:teamcity_url) + self.password = nil + end + end + def title 'JetBrains TeamCity CI' end diff --git a/app/models/repository.rb b/app/models/repository.rb index 2c5ab62d22c..8b51602bc23 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -373,11 +373,25 @@ class Repository @root_ref ||= raw_repository.root_ref end - def commit_file(user, path, content, message, branch) + def commit_dir(user, path, message, branch) commit_with_hooks(user, branch) do |ref| - path[0] = '' if path[0] == '/' + committer = user_to_committer(user) + options = {} + options[:committer] = committer + options[:author] = committer - committer = user_to_comitter(user) + options[:commit] = { + message: message, + branch: ref, + } + + raw_repository.mkdir(path, options) + end + end + + def commit_file(user, path, content, message, branch, update) + commit_with_hooks(user, branch) do |ref| + committer = user_to_committer(user) options = {} options[:committer] = committer options[:author] = committer @@ -388,7 +402,8 @@ class Repository options[:file] = { content: content, - path: path + path: path, + update: update } Gitlab::Git::Blob.commit(raw_repository, options) @@ -397,9 +412,7 @@ class Repository def remove_file(user, path, message, branch) commit_with_hooks(user, branch) do |ref| - path[0] = '' if path[0] == '/' - - committer = user_to_comitter(user) + committer = user_to_committer(user) options = {} options[:committer] = committer options[:author] = committer @@ -416,7 +429,7 @@ class Repository end end - def user_to_comitter(user) + def user_to_committer(user) { email: user.email, name: user.name, diff --git a/app/models/service.rb b/app/models/service.rb index 60fcc9d2857..7e845d565b1 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -117,6 +117,15 @@ class Service < ActiveRecord::Base end end + # ActiveRecord does not provide a mechanism to track changes in serialized keys. + # This is why we need to perform extra query to do it mannually. + def prop_updated?(prop_name) + relation_name = self.type.underscore + previous_value = project.send(relation_name).send(prop_name) + return false if previous_value.nil? + previous_value != send(prop_name) + end + def async_execute(data) return unless supported_events.include?(data[:object_kind]) diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb index fc1ae5774d5..479a2d6defc 100644 --- a/app/services/ci/create_commit_service.rb +++ b/app/services/ci/create_commit_service.rb @@ -17,8 +17,10 @@ module Ci tag = origin_ref.start_with?('refs/tags/') commit = project.gl_project.ensure_ci_commit(sha) - commit.update_committed! - commit.create_builds(ref, tag, user) + unless commit.skip_ci? + commit.update_committed! + commit.create_builds(ref, tag, user) + end commit end diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb new file mode 100644 index 00000000000..71272fb5707 --- /dev/null +++ b/app/services/files/create_dir_service.rb @@ -0,0 +1,9 @@ +require_relative "base_service" + +module Files + class CreateDirService < Files::BaseService + def commit + repository.commit_dir(current_user, @file_path, @commit_message, @target_branch) + end + end +end diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index ffbb5993279..c8e3a910bba 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -3,7 +3,7 @@ require_relative "base_service" module Files class CreateService < Files::BaseService def commit - repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch) + repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, false) end def validate diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index a20903c6f02..1960dc7d949 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -3,7 +3,7 @@ require_relative "base_service" module Files class UpdateService < Files::BaseService def commit - repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch) + repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, true) end end end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index fcc0f2a6a8d..7963af127e1 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -29,7 +29,7 @@ module MergeRequests private def commit - committer = repository.user_to_comitter(current_user) + committer = repository.user_to_committer(current_user) options = { message: commit_message, diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index c327c244f0d..64ea6dd42eb 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -27,6 +27,7 @@ module Projects def transfer(project, new_namespace) Project.transaction do old_path = project.path_with_namespace + old_namespace = project.namespace new_path = File.join(new_namespace.try(:path) || '', project.path) if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present? @@ -51,6 +52,9 @@ module Projects # clear project cached events project.reset_events_cache + # Move uploads + Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path) + true end end diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index 60235b6be2a..9a5fe4af9dd 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -54,6 +54,7 @@ class SystemHooksService data.merge!({ project_name: model.project.name, project_path: model.project.path, + project_path_with_namespace: model.project.path_with_namespace, project_id: model.project.id, user_name: model.user.name, user_email: model.user.email, diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index f9673abbfe8..e8211585834 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -26,7 +26,7 @@ class FileUploader < CarrierWave::Uploader::Base end def secure_url - File.join(Gitlab.config.gitlab.url, @project.path_with_namespace, "uploads", @secret, file.filename) + File.join("/uploads", @secret, file.filename) end def file_storage? diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index e3698ac1c46..bc08458312c 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -32,7 +32,7 @@ %hr = form_tag admin_users_path, method: :get, class: 'form-inline' do .form-group - = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control' + = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false = hidden_field_tag "filter", params[:filter] = button_tag class: 'btn btn-primary' do %i.fa.fa-search @@ -54,19 +54,19 @@ %b.caret %ul.dropdown-menu %li - = link_to admin_users_path(sort: sort_value_name) do + = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do = sort_title_name - = link_to admin_users_path(sort: sort_value_recently_signin) do + = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do = sort_title_recently_signin - = link_to admin_users_path(sort: sort_value_oldest_signin) do + = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do = sort_title_oldest_signin - = link_to admin_users_path(sort: sort_value_recently_created) do + = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do = sort_title_recently_created - = link_to admin_users_path(sort: sort_value_oldest_created) do + = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do = sort_title_oldest_created - = link_to admin_users_path(sort: sort_value_recently_updated) do + = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do = sort_title_recently_updated - = link_to admin_users_path(sort: sort_value_oldest_updated) do + = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do = sort_title_oldest_updated = link_to 'New User', new_admin_user_path, class: "btn btn-new btn-sm" diff --git a/app/views/ci/admin/runners/index.html.haml b/app/views/ci/admin/runners/index.html.haml index b9d6703ff41..01ce81b4476 100644 --- a/app/views/ci/admin/runners/index.html.haml +++ b/app/views/ci/admin/runners/index.html.haml @@ -27,7 +27,7 @@ .pull-left = form_tag ci_admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do .form-group - = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token' + = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token', spellcheck: false = submit_tag 'Search', class: 'btn' .pull-right.light diff --git a/app/views/ci/admin/runners/show.html.haml b/app/views/ci/admin/runners/show.html.haml index 5bb442cbf92..92787b2e6ac 100644 --- a/app/views/ci/admin/runners/show.html.haml +++ b/app/views/ci/admin/runners/show.html.haml @@ -76,7 +76,7 @@ %td = form_tag ci_admin_runner_path(@runner), id: 'runner-projects-search', class: 'form-inline', method: :get do .form-group - = search_field_tag :search, params[:search], class: 'form-control' + = search_field_tag :search, params[:search], class: 'form-control', spellcheck: false = submit_tag 'Search', class: 'btn' %td diff --git a/app/views/ci/projects/_info.html.haml b/app/views/ci/projects/_info.html.haml deleted file mode 100644 index 1888e1bde93..00000000000 --- a/app/views/ci/projects/_info.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -- if no_runners_for_project?(@project) - = render 'no_runners' diff --git a/app/views/ci/projects/show.html.haml b/app/views/ci/projects/show.html.haml deleted file mode 100644 index 888b1ea41d5..00000000000 --- a/app/views/ci/projects/show.html.haml +++ /dev/null @@ -1,60 +0,0 @@ -= render 'ci/shared/guide' unless @project.setup_finished? - -- if current_user && can?(current_user, :manage_project, gl_project) && !@project.any_runners? - .alert.alert-danger - Builds for this project wont be served unless you configure runners on - = link_to "Runners page", runners_path(@project.gl_project) - -%ul.nav.nav-tabs.append-bottom-20 - %li{class: ref_tab_class} - = link_to 'All commits', ci_project_path(@project) - - @project.tracked_refs.each do |ref| - %li{class: ref_tab_class(ref)} - = link_to ref, ci_project_path(@project, ref: ref) - - - if @ref && !@project.tracked_refs.include?(@ref) - %li{class: 'active'} - = link_to @ref, ci_project_path(@project, ref: @ref) - - %li.pull-right - = link_to 'Go to project', project_path(gl_project), class: 'btn btn-sm' - -- if @ref - %p - Paste build status image for #{@ref} with next link - = link_to '#', class: 'badge-codes-toggle btn btn-default btn-xs' do - Status Badge - .badge-codes-block.bs-callout.bs-callout-info.hide - %p - Status badge for - %span.label.label-info #{@ref} - branch - %div - %label Markdown: - = text_field_tag 'badge_md', markdown_badge_code(@project, @ref), readonly: true, class: 'form-control' - %label Html: - = text_field_tag 'badge_html', html_badge_code(@project, @ref), readonly: true, class: 'form-control' - - - - -%table.table.builds - %thead - %tr - %th Status - %th Commit - %th Message - %th Branch - %th Total duration - %th Finished at - - if @project.coverage_enabled? - %th Coverage - - = render @commits - -= paginate @commits - -- if @commits.empty? - .bs-callout - %h4 No commits yet - diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml index 19d919f9b6a..f98fd9f06ba 100644 --- a/app/views/dashboard/_activities.html.haml +++ b/app/views/dashboard/_activities.html.haml @@ -3,10 +3,9 @@ .gray-content-block - if current_user - %ul.nav.nav-pills.event_filter.pull-right - %li.pull-right - = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'rss-btn' do - %i.fa.fa-rss + .pull-right + = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do + %i.fa.fa-rss = render 'shared/event_filter' .content_list diff --git a/app/views/dashboard/projects/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml index e09e032a7f1..81a5909e2d2 100644 --- a/app/views/dashboard/projects/_projects.html.haml +++ b/app/views/dashboard/projects/_projects.html.haml @@ -1,10 +1,11 @@ .projects-list-holder .projects-search-form .input-group - = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' + = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false - if current_user.can_create_project? %span.input-group-btn = link_to new_project_path, class: 'btn btn-green' do - New project + %i.fa.fa-plus + New Project = render 'shared/projects/list', projects: @projects, ci: true diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index 83d4d321c83..fcb07b04083 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -11,7 +11,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", id: "groups_search" + = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "groups_search", spellcheck: false .form-group = button_tag 'Search', class: "btn btn-default" diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index 5a3d689d1e5..2761272aa8a 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", id: "projects_search" + = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "projects_search", spellcheck: false .form-group = button_tag 'Search', class: "btn btn-success" diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 2b27a88794d..11d69977ef9 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -1,10 +1,11 @@ .panel.panel-default.projects-list-holder .panel-heading.clearfix .input-group - = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' + = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false - if can? current_user, :create_projects, @group %span.input-group-btn = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-green' do - New project + %i.fa.fa-plus + New Project - = render 'shared/projects/list', projects: @projects, projects_limit: 20, stars: false + = render 'shared/projects/list', projects: @projects, projects_limit: 20, stars: false, skip_namespace: true diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 3a6d07ebddf..fee4b0052b5 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -12,7 +12,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' } + = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input', spellcheck: false } = button_tag 'Search', class: 'btn' - if current_user && current_user.can?(:admin_group_member, @group) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index a9ba9d2ba10..dc8e81323a6 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -25,11 +25,9 @@ .hidden-xs - if current_user = render "events/event_last_push", event: @last_push - - %ul.nav.nav-pills.event_filter.pull-right - %li - = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'rss-btn' do - %i.fa.fa-rss + .pull-right + = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'btn rss-btn' do + %i.fa.fa-rss = render 'shared/event_filter' %hr diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 7c89457ace3..2169a821fb2 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -14,6 +14,8 @@ = link_to 'Lists', '#lists' %li = link_to 'Tables', '#tables' + %li + = link_to 'Nav', '#nav' %li = link_to 'Buttons', '#buttons' %li @@ -30,16 +32,31 @@ %h2#blocks Blocks %h3 - %code .well + %code .gray-content-block - .well - %h4 Something + + .gray-content-block.middle-block + %h4 Normal block inside content + = lorem + + .gray-content-block.second-block + %h4 Second block = lorem %h2#lists Lists + %h3 + %code .content-list + %ul.content-list + %li + One item + %li + One item + %li + One item + %h3 %code .well-list %ul.well-list @@ -102,11 +119,40 @@ %td the Bird %td @twitter + %h2#navs Navigation + + %h3 + %code .center-top-menu + .example + %ul.center-top-menu + %li.active + %a Open + %li + %a Closed + + %h3 + %code .btn-group.btn-group-next + .example + %div.btn-group.btn-group-next + %a.btn.active Open + %a.btn Closed + + + %h3 + %code .nav.nav-tabs + .example + %ul.nav.nav-tabs + %li.active + %a Open + %li + %a Closed + %h2#buttons Buttons .example %button.btn.btn-default{:type => "button"} Default + %button.btn.btn-gray{:type => "button"} Gray %button.btn.btn-primary{:type => "button"} Primary %button.btn.btn-success{:type => "button"} Success %button.btn.btn-info{:type => "button"} Info diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 2468687b56d..1a883e20e89 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -6,7 +6,7 @@ = brand_header_logo .gitlab-text-container %h3 GitLab - + - if defined?(sidebar) && sidebar = render "layouts/nav/#{sidebar}" - elsif current_user @@ -23,6 +23,7 @@ = current_user.username .content-wrapper = render "layouts/flash" + = yield :flash_message %div{ class: container_class } .content .clearfix diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index e2d2dec7ab8..ceb64ce3157 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,6 +1,6 @@ .search = form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f| - = search_field_tag "search", nil, placeholder: search_placeholder, class: "search-input form-control" + = search_field_tag "search", nil, placeholder: search_placeholder, class: "search-input form-control", spellcheck: false = hidden_field_tag :group_id, @group.try(:id) - if @project && @project.persisted? = hidden_field_tag :project_id, @project.id diff --git a/app/views/layouts/ci/_nav_project.html.haml b/app/views/layouts/ci/_nav_project.html.haml index 545abc23d99..f094edbfa87 100644 --- a/app/views/layouts/ci/_nav_project.html.haml +++ b/app/views/layouts/ci/_nav_project.html.haml @@ -5,17 +5,6 @@ %span Back to project %li.separate-item - = nav_link path: ['projects#show', 'commits#show', 'builds#show'] do - = link_to ci_project_path(@project) do - = icon('list-alt fw') - %span - Commits - %span.count= @project.commits.count - = nav_link path: ['services#index', 'services#edit'] do - = link_to ci_project_services_path(@project) do - = icon('share fw') - %span - Services = nav_link path: 'events#index' do = link_to ci_project_events_path(@project) do = icon('book fw') diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index a218ec7486c..e4c285d8023 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -32,7 +32,7 @@ Files - if project_nav_tab? :commits - = nav_link(controller: %w(commit commits compare repositories tags branches)) do + = nav_link(controller: %w(commit commits compare repositories tags branches builds)) do = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits', data: {placement: 'right'} do = icon('history fw') %span @@ -76,13 +76,6 @@ Merge Requests %span.count.merge_counter= @project.merge_requests.opened.count - - if @project.gitlab_ci? - = nav_link(controller: [:ci, :project]) do - = link_to ci_project_path(@project.gitlab_ci_project), title: 'Continuous Integration', data: {placement: 'right'} do - = icon('building fw') - %span - Continuous Integration - - if project_nav_tab? :settings = nav_link(controller: [:project_members, :teams]) do = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab', data: {placement: 'right'} do diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 9279a846623..954dbe5d2b9 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -60,3 +60,13 @@ = icon('building fw') %span CI Settings + = nav_link controller: 'ci_services' do + = link_to namespace_project_ci_services_path(@project.namespace, @project) do + = icon('share fw') + %span + CI Services + = nav_link path: 'events#index' do + = link_to ci_project_events_path(@project.gitlab_ci_project) do + = icon('book fw') + %span + CI Events diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index e0ae4d9720f..0ca8bd95157 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -18,5 +18,6 @@ %code.key-fingerprint= @key.fingerprint %pre.well-pre = @key.key - .pull-right - = link_to 'Remove', path_to_key(@key, is_admin), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key" + .col-md-12 + .pull-right + = link_to 'Remove', path_to_key(@key, is_admin), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key" diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index 1261f6254d7..c2683bc6219 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -1,10 +1,9 @@ = render 'projects/last_push' .gray-content-block.activity-filter-block - if current_user - %ul.nav.nav-pills.event_filter.pull-right - %li - = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'rss-btn' do - %i.fa.fa-rss + .pull-right + = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do + %i.fa.fa-rss = render 'shared/event_filter' .content_list{:"data-href" => activity_project_path(@project)} diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index b4c7d8b9b71..a1ae1397584 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -19,7 +19,7 @@ - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) = render blob_commit, project: @project -%div#tree-content-holder.tree-content-holder +%div#blob-content-holder.blob-content-holder %article.file-holder .file-title = blob_icon blob.mode, blob.name diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 9c3e1703c89..f1ad0c3c403 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -11,7 +11,7 @@ - if current_action?(:new) || current_action?(:create) \/ = text_field_tag 'file_name', params[:file_name], placeholder: "File name", - required: true, class: 'form-control new-file-name' + required: true, class: 'form-control new-file-name js-quick-submit' .pull-right = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'form-control' diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml new file mode 100644 index 00000000000..cb1567a2e68 --- /dev/null +++ b/app/views/projects/blob/_new_dir.html.haml @@ -0,0 +1,25 @@ +#modal-create-new-dir.modal + .modal-dialog + .modal-content + .modal-header + %a.close{href: "#", "data-dismiss" => "modal"} × + %h3.page-title Create New Directory + .modal-body + = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, id: 'dir-create-form', class: 'form-horizontal' do + .form-group + = label_tag :dir_name, 'Directory Name', class: 'control-label' + .col-sm-10 + = text_field_tag :dir_name, params[:dir_name], placeholder: "Directory name", required: true, class: 'form-control' + = render 'shared/commit_message_container', params: params, placeholder: '' + - unless @project.empty_repo? + .form-group + = label_tag :branch_name, 'Branch', class: 'control-label' + .col-sm-10 + = text_field_tag 'new_branch', @ref, class: "form-control" + .form-group + .col-sm-offset-2.col-sm-10 + = submit_tag "Create directory", class: 'btn btn-primary btn-create' + = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" + +:coffeescript + disableButtonIfAnyEmptyField($("#dir-create-form"), ".form-control", ".btn-create"); diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index 1a1df127703..e27f1707527 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -4,9 +4,6 @@ .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × %h3.page-title #{title} - %p.light - From branch - %strong= @ref .modal-body = form_tag form_path, method: method, class: 'blob-file-upload-form-js form-horizontal' do .dropzone @@ -18,6 +15,12 @@ .dropzone-alerts{class: "alert alert-danger data", style: "display:none"} = render 'shared/commit_message_container', params: params, placeholder: placeholder + - 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" .form-group .col-sm-offset-2.col-sm-10 = button_tag button_title, class: 'btn btn-small btn-primary btn-upload-file', id: 'submit-all' diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 1950586b112..7975137c37f 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -2,12 +2,7 @@ = render "header_title" .gray-content-block.top-block - Create a new file or - = link_to 'upload', '#modal-upload-blob', - { class: 'upload-link', 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} - an existing one - -= render 'projects/blob/upload', title: 'Upload', placeholder: 'Upload new file', button_title: 'Upload file', form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post + Create a new file .file-editor = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal form-new-file js-requires-input') do @@ -20,7 +15,7 @@ = label_tag 'branch', class: 'control-label' do Branch .col-sm-10 - = text_field_tag 'new_branch', @ref, class: "form-control" + = text_field_tag 'new_branch', @ref, class: "form-control js-quick-submit" = hidden_field_tag 'content', '', id: 'file-content' = render 'projects/commit_button', ref: @ref, diff --git a/app/views/projects/builds/_build.html.haml b/app/views/projects/builds/_build.html.haml deleted file mode 100644 index 21c543b38dd..00000000000 --- a/app/views/projects/builds/_build.html.haml +++ /dev/null @@ -1,50 +0,0 @@ -- gl_project = build.project.gl_project -%tr.build - %td.status - = ci_status_with_icon(build.status) - - %td.build-link - = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do - %strong Build ##{build.id} - - - if defined?(ref) - %td - = build.ref - - %td - = build.stage - - %td - = build.name - .pull-right - - if build.tags.any? - - build.tag_list.each do |tag| - %span.label.label-primary - = tag - - if build.trigger_request - %span.label.label-info triggered - - if build.allow_failure - %span.label.label-danger allowed to fail - - %td.duration - - if build.duration - #{duration_in_words(build.finished_at, build.started_at)} - - %td.timestamp - - if build.finished_at - %span #{time_ago_in_words build.finished_at} ago - - - if build.project.coverage_enabled? - %td.coverage - - if build.coverage - #{build.coverage}% - - %td - - if defined?(controls) && current_user && can?(current_user, :manage_builds, gl_project) - .pull-right - - if build.active? - = link_to cancel_ci_project_build_path(build.project, build, return_to: request.original_url), title: 'Cancel build' do - %i.fa.fa-remove.cred - - elsif build.commands.present? - = link_to retry_ci_project_build_path(build.project, build, return_to: request.original_url), method: :post, title: 'Retry build' do - %i.fa.fa-repeat diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 93cd4dcfd93..9c3ae622b72 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -9,7 +9,7 @@ #up-build-trace - if @commit.matrix_for_ref?(@build.ref) %ul.center-top-menu.build-top-menu - - @commit.builds_without_retry_for_ref(@build.ref).each do |build| + - @commit.latest_builds_for_ref(@build.ref).each do |build| %li{class: ('active' if build == @build) } = link_to namespace_project_build_path(@project.namespace, @project, build) do = ci_icon_for_status(build.status) @@ -20,7 +20,7 @@ = build.id - - unless @commit.builds_without_retry_for_ref(@build.ref).include?(@build) + - unless @commit.latest_builds_for_ref(@build.ref).include?(@build) %li.active %a Build ##{@build.id} @@ -72,9 +72,9 @@ - if current_user && can?(current_user, :manage_builds, @project) .pull-right - if @build.active? - = link_to "Cancel", cancel_ci_project_build_path(@ci_project, @build), class: 'btn btn-sm btn-danger' + = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-danger' - elsif @build.commands.present? - = link_to "Retry", retry_ci_project_build_path(@ci_project, @build), class: 'btn btn-sm btn-primary', method: :post + = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary', method: :post - if @build.duration %p diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index 4b69a6d7a6f..3bc2daeec4e 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -1,6 +1,6 @@ - return unless @membership -= form_tag profile_notifications_path, method: :put, remote: true, class: 'inline-form', id: 'notification-form' do += form_tag profile_notifications_path, method: :put, remote: true, class: 'inline', id: 'notification-form' do = hidden_field_tag :notification_type, 'project' = hidden_field_tag :notification_id, @membership.id = hidden_field_tag :notification_level diff --git a/app/views/ci/services/_form.html.haml b/app/views/projects/ci_services/_form.html.haml similarity index 80% rename from app/views/ci/services/_form.html.haml rename to app/views/projects/ci_services/_form.html.haml index 9110aaa0528..397832e56db 100644 --- a/app/views/ci/services/_form.html.haml +++ b/app/views/projects/ci_services/_form.html.haml @@ -4,13 +4,10 @@ %p= @service.description -.back-link - = link_to ci_project_services_path(@project) do - ← to services %hr -= form_for(@service, as: :service, url: ci_project_service_path(@project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f| += form_for(@service, as: :service, url: namespace_project_ci_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f| - if @service.errors.any? .alert.alert-danger %ul @@ -54,4 +51,4 @@ = f.submit 'Save', class: 'btn btn-save'   - if @service.valid? && @service.activated? && @service.can_test? - = link_to 'Test settings', test_ci_project_service_path(@project, @service.to_param), class: 'btn' + = link_to 'Test settings', test_namespace_project_ci_service_path(@project.namespace, @project, @service.to_param), class: 'btn' diff --git a/app/views/ci/services/edit.html.haml b/app/views/projects/ci_services/edit.html.haml similarity index 100% rename from app/views/ci/services/edit.html.haml rename to app/views/projects/ci_services/edit.html.haml diff --git a/app/views/ci/services/index.html.haml b/app/views/projects/ci_services/index.html.haml similarity index 81% rename from app/views/ci/services/index.html.haml rename to app/views/projects/ci_services/index.html.haml index 37e5723b541..c78b21884a3 100644 --- a/app/views/ci/services/index.html.haml +++ b/app/views/projects/ci_services/index.html.haml @@ -13,7 +13,7 @@ %td = boolean_to_icon service.activated? %td - = link_to edit_ci_project_service_path(@project, service.to_param) do + = link_to edit_namespace_project_ci_service_path(@project.namespace, @project, service.to_param) do %strong= service.title %td = service.description diff --git a/app/views/projects/ci_settings/_form.html.haml b/app/views/projects/ci_settings/_form.html.haml index 9f891f557a9..d711413c6b9 100644 --- a/app/views/projects/ci_settings/_form.html.haml +++ b/app/views/projects/ci_settings/_form.html.haml @@ -8,6 +8,22 @@ Edit your #{link_to ".gitlab-ci.yml using web-editor", yaml_web_editor_link(@ci_project)} +- unless @project.empty_repo? + %p + Paste build status image for #{@repository.root_ref} with next link + = link_to '#', class: 'badge-codes-toggle btn btn-default btn-xs' do + Status Badge + .badge-codes-block.bs-callout.bs-callout-info.hide + %p + Status badge for + %span.label.label-info #{@ref} + branch + %div + %label Markdown: + = text_field_tag 'badge_md', markdown_badge_code(@ci_project, @repository.root_ref), readonly: true, class: 'form-control' + %label Html: + = text_field_tag 'badge_html', html_badge_code(@ci_project, @repository.root_ref), readonly: true, class: 'form-control' + = nested_form_for @ci_project, url: namespace_project_ci_settings_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f| - if @ci_project.errors.any? #error_explanation diff --git a/app/views/ci/projects/_no_runners.html.haml b/app/views/projects/ci_settings/_no_runners.html.haml similarity index 87% rename from app/views/ci/projects/_no_runners.html.haml rename to app/views/projects/ci_settings/_no_runners.html.haml index c0a296fb17d..33038c52978 100644 --- a/app/views/ci/projects/_no_runners.html.haml +++ b/app/views/projects/ci_settings/_no_runners.html.haml @@ -4,5 +4,5 @@ %br You can add Specific runner for this project on Runners page - - if current_user.is_admin + - if current_user.admin or add Shared runner for whole application in admin are. diff --git a/app/views/projects/ci_settings/edit.html.haml b/app/views/projects/ci_settings/edit.html.haml index e9040fe4337..eedf484bf00 100644 --- a/app/views/projects/ci_settings/edit.html.haml +++ b/app/views/projects/ci_settings/edit.html.haml @@ -6,6 +6,9 @@ yaml file which is based on your old jobs. Put this file to the root of your project and name it .gitlab-ci.yml +- if no_runners_for_project?(@ci_project) + = render 'no_runners' + = render 'form' - if @ci_project.generated_yaml_config diff --git a/app/views/projects/commit/ci.html.haml b/app/views/projects/commit/ci.html.haml index f4382e88046..ca71a91af15 100644 --- a/app/views/projects/commit/ci.html.haml +++ b/app/views/projects/commit/ci.html.haml @@ -3,11 +3,6 @@ = render "commit_box" = render "ci_menu" -- if @ci_project && current_user && can?(current_user, :manage_builds, @project) - .pull-right - - if @ci_commit.builds.running_or_pending.any? - = link_to "Cancel", cancel_ci_project_commits_path(@ci_project, @ci_commit), class: 'btn btn-sm btn-danger' - - if @ci_commit.yaml_errors.present? .bs-callout.bs-callout-danger @@ -20,30 +15,39 @@ .bs-callout.bs-callout-warning \.gitlab-ci.yml not found in this commit -- @ci_commit.refs.each do |ref| +.gray-content-block.second-block + Latest builds + + .pull-right + - if @ci_commit.duration > 0 + %i.fa.fa-time + #{time_interval_in_words @ci_commit.duration} + +   + + - if @ci_project && current_user && can?(current_user, :manage_builds, @project) + - if @ci_commit.builds.running_or_pending.any? + = link_to "Cancel all", cancel_builds_namespace_project_commit_path(@project.namespace, @project, @commit.sha), class: 'btn btn-xs btn-danger' + +%table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Ref + %th Stage + %th Name + %th Duration + %th Finished at + - if @ci_project && @ci_project.coverage_enabled? + %th Coverage + %th + - @ci_commit.refs.each do |ref| + = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered, + locals: { coverage: @ci_project.try(:coverage_enabled?), allow_retry: true } + +- if @ci_commit.retried.any? .gray-content-block.second-block - Builds for #{ref} - - if @ci_commit.duration_for_ref(ref) > 0 - %small.pull-right - %i.fa.fa-time - #{time_interval_in_words @ci_commit.duration_for_ref(ref)} - - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Stage - %th Name - %th Duration - %th Finished at - - if @ci_project && @ci_project.coverage_enabled? - %th Coverage - %th - = render partial: "projects/builds/build", collection: @ci_commit.builds_without_retry.for_ref(ref), controls: true - -- if @ci_commit.retried_builds.any? - %h3 Retried builds %table.table.builds @@ -59,4 +63,5 @@ - if @ci_project && @ci_project.coverage_enabled? %th Coverage %th - = render partial: "projects/builds/build", collection: @ci_commit.retried_builds, ref: true + = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, + locals: { coverage: @ci_project.try(:coverage_enabled?) } diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml new file mode 100644 index 00000000000..7314f8e79d3 --- /dev/null +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -0,0 +1,51 @@ +%tr.commit_status + %td.status + = ci_status_with_icon(commit_status.status) + + %td.commit_status-link + - if commit_status.target_url + = link_to commit_status.target_url do + %strong Build ##{commit_status.id} + - else + %strong Build ##{commit_status.id} + + %td + = commit_status.ref + + %td + = commit_status.stage + + %td + = commit_status.name + .pull-right + - if commit_status.tags.any? + - commit_status.tags.each do |tag| + %span.label.label-primary + = tag + - if commit_status.try(:trigger_request) + %span.label.label-info triggered + - if commit_status.try(:allow_failure) + %span.label.label-danger allowed to fail + + %td.duration + - if commit_status.duration + #{duration_in_words(commit_status.finished_at, commit_status.started_at)} + + %td.timestamp + - if commit_status.finished_at + %span #{time_ago_in_words commit_status.finished_at} ago + + - if defined?(coverage) && coverage + %td.coverage + - if commit_status.try(:coverage) + #{commit_status.coverage}% + + %td + .pull-right + - if current_user && can?(current_user, :manage_builds, commit_status.gl_project) + - if commit_status.cancel_url + = link_to commit_status.cancel_url, title: 'Cancel' do + %i.fa.fa-remove.cred + - elsif defined?(allow_retry) && allow_retry && commit_status.retry_url + = link_to commit_status.retry_url, method: :post, title: 'Retry' do + %i.fa.fa-repeat diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 4617b188150..9698921f6da 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -16,7 +16,7 @@ - if diff_file.mode_changed? %span.file-mode= "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" - .diff-btn-group + .diff-controls - if blob.text? = link_to '#', class: 'js-toggle-diff-comments btn btn-sm active has_tooltip', title: "Toggle comments for this file" do %i.fa.fa-comments diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 90dce739992..1882a82fba5 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -193,13 +193,13 @@ .panel.panel-default.panel.panel-danger .panel-heading Remove project .panel-body - = form_tag(namespace_project_path(@project.namespace, @project), method: :delete, html: { class: 'form-horizontal'}) do + = form_tag(namespace_project_path(@project.namespace, @project), method: :delete, class: 'form-horizontal') do %p Removing the project will delete its repository and all related resources including issues, merge requests etc. %br %strong Removed projects cannot be restored! - = link_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } + = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } - else .nothing-here-block Only project owner can remove a project diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml index cd5f3a5d39e..f0b0a11c04a 100644 --- a/app/views/projects/forks/new.html.haml +++ b/app/views/projects/forks/new.html.haml @@ -1,36 +1,41 @@ - page_title "Fork project" -%h3.page-title Fork project -%p.lead - Click to fork the project to a user or group -%hr +- if @namespaces.present? + %h3.page-title Fork project + %p.lead + Click to fork the project to a user or group + %hr -.fork-namespaces - - @namespaces.in_groups_of(6, false) do |group| - .row - - group.each do |namespace| - .col-md-2.col-sm-3 - - if fork = namespace.find_fork_of(@project) - .fork-thumbnail - = link_to project_path(fork), title: "Visit project fork", class: 'has_tooltip' do - = image_tag namespace_icon(namespace, 100) - .caption - %strong - = namespace.human_name - %div.text-primary - Already forked + .fork-namespaces + - @namespaces.in_groups_of(6, false) do |group| + .row + - group.each do |namespace| + .col-md-2.col-sm-3 + - if fork = namespace.find_fork_of(@project) + .fork-thumbnail + = link_to project_path(fork), title: "Visit project fork", class: 'has_tooltip' do + = image_tag namespace_icon(namespace, 100) + .caption + %strong + = namespace.human_name + %div.text-primary + Already forked - - else - .fork-thumbnail - = link_to namespace_project_fork_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do - = image_tag namespace_icon(namespace, 100) - .caption - %strong - = namespace.human_name + - else + .fork-thumbnail + = link_to namespace_project_fork_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do + = image_tag namespace_icon(namespace, 100) + .caption + %strong + = namespace.human_name - %p.light - Fork is a copy of a project repository. - %br - Forking a repository allows you to do changes without affecting the original project. + %p.light + Fork is a copy of a project repository. + %br + Forking a repository allows you to do changes without affecting the original project. +- else + %h3 No available namespaces to fork the project + %p.slead + You must have permission to create a project in a namespace before forking. .save-project-loader.hide .center diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index 534c545329b..4cf13492e99 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -10,7 +10,7 @@ .form-group = f.label :title, class: 'control-label' .col-sm-10 - = f.text_field :title, class: "form-control", required: true + = f.text_field :title, class: "form-control js-quick-submit", required: true .form-group = f.label :color, "Background Color", class: 'control-label' .col-sm-10 diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 0b0f52c653c..e7ac7a0eaa4 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -24,7 +24,7 @@ %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) - .light + .normal %span Request to merge %span.label-branch #{source_branch_with_namespace(@merge_request)} %span into @@ -34,7 +34,7 @@ = render "projects/merge_requests/widget/show.html.haml" - if @merge_request.open? && @merge_request.can_be_merged? - .light + .light.append-bottom-20 You can also accept this merge request manually using the = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 74e9668052d..255ddab479f 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -16,13 +16,13 @@ .form-group = f.label :title, "Title", class: "control-label" .col-sm-10 - = f.text_field :title, maxlength: 255, class: "form-control", required: true + = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true %p.hint Required .form-group.milestone-description = f.label :description, "Description", class: "control-label" .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do - = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit' .hint .pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}. .pull-left Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index bccea21e7a8..daab2326bc7 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -8,7 +8,7 @@ = form_for @project, html: { class: 'new_project form-horizontal js-requires-input' } do |f| .form-group.project-name-holder = f.label :path, class: 'control-label' do - %strong Project path + Project path .col-sm-10 .input-group = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 1, autofocus: true, required: true @@ -23,7 +23,6 @@ = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user), {}, {class: 'select2', tabindex: 2} - if import_sources_enabled? - %hr .project-import.js-toggle-container .form-group @@ -35,7 +34,7 @@ %i.fa.fa-github GitHub - else - = link_to '#', class: 'how_to_import_link light btn import_github' do + = link_to '#', class: 'how_to_import_link btn import_github' do %i.fa.fa-github GitHub = render 'github_import_modal' @@ -46,7 +45,7 @@ %i.fa.fa-bitbucket Bitbucket - else - = link_to status_import_bitbucket_path, class: 'how_to_import_link light btn import_bitbucket', "data-no-turbolink" => "true" do + = link_to status_import_bitbucket_path, class: 'how_to_import_link btn import_bitbucket', "data-no-turbolink" => "true" do %i.fa.fa-bitbucket Bitbucket = render 'bitbucket_import_modal' @@ -57,7 +56,7 @@ %i.fa.fa-heart GitLab.com - else - = link_to status_import_gitlab_path, class: 'how_to_import_link light btn import_gitlab' do + = link_to status_import_gitlab_path, class: 'how_to_import_link btn import_gitlab' do %i.fa.fa-heart GitLab.com = render 'gitlab_import_modal' @@ -97,7 +96,7 @@ %li To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}. - %hr.prepend-botton-10 + .prepend-botton-10 .form-group = f.label :description, class: 'control-label' do @@ -112,10 +111,11 @@ - if current_user.can_create_group? .pull-right - .light - Need a group for several dependent projects? - = link_to new_group_path, class: "btn btn-xs" do - Create a group + .light.inline + .space-right + Need a group for several dependent projects? + = link_to new_group_path, class: "btn" do + Create a group .save-project-loader.hide .center diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index a0e26f9827e..a21c019986a 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -2,7 +2,7 @@ = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true do |f| = note_target_fields(note) = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do - = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field' + = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field js-quick-submit' = render 'projects/notes/hints' .note-form-actions diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index d99445da59a..13dfa0a1bb3 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -8,12 +8,12 @@ = f.hidden_field :noteable_type = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text' + = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-quick-submit' = render 'projects/notes/hints' .error-alert .note-form-actions .buttons.clearfix - = f.submit 'Add Comment', class: "btn comment-btn btn-grouped js-comment-button" + = f.submit 'Add Comment', class: "btn btn-green comment-btn btn-grouped js-comment-button" = yield(:note_actions) %a.btn.grouped.js-close-discussion-note-form Cancel diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 9a0a824b811..82809bec5b8 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -5,7 +5,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' } + = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input', spellcheck: false } = button_tag 'Search', class: 'btn' - if can?(current_user, :admin_project_member, @project) diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index b9486a9b492..07c24950ee2 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -3,10 +3,10 @@ - split_button = split_button || false - if split_button == true %span.btn-group{class: btn_class} - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn col-xs-10', rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn btn-success col-xs-10', rel: 'nofollow' do %i.fa.fa-download %span Download zip - %a.col-xs-2.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } + %a.col-xs-2.btn.btn-success.dropdown-toggle{ 'data-toggle' => 'dropdown' } %span.caret %span.sr-only Select Archive Format diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index efa119edd5a..e95d987d74c 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -2,9 +2,10 @@ - if current_user = auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity") -- if current_user && can?(current_user, :download_code, @project) - = render 'shared/no_ssh' - = render 'shared/no_password' += content_for :flash_message do + - if current_user && can?(current_user, :download_code, @project) + = render 'shared/no_ssh' + = render 'shared/no_password' - if prefer_readme? = render 'projects/last_push' diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml index f082d711865..7e9af19c8ba 100644 --- a/app/views/projects/tree/_readme.html.haml +++ b/app/views/projects/tree/_readme.html.haml @@ -1,7 +1,8 @@ -%article.readme-holder#README - = link_to '#README' do - %h4.readme-file-title - %i.fa.fa-file - = readme.name - .wiki +%article.file-holder.readme-holder#README + .file-title + = link_to '#README' do + %strong + %i.fa.fa-file + = readme.name + .file-content.wiki = render_readme(readme) diff --git a/app/views/projects/tree/_tree.html.haml b/app/views/projects/tree/_tree.html.haml index 367a87927d7..7ff48e32e60 100644 --- a/app/views/projects/tree/_tree.html.haml +++ b/app/views/projects/tree/_tree.html.haml @@ -1,51 +1,71 @@ -%ul.breadcrumb.repo-breadcrumb - %li - = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do - = @project.path - - tree_breadcrumbs(tree, 6) do |title, path| +.gray-content-block + %ul.breadcrumb.repo-breadcrumb %li - - if path - = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path) - - else - = link_to title, '#' - - if current_user && can_push_branch?(@project, @ref) - %li - = link_to namespace_project_new_blob_path(@project.namespace, @project, @id), title: 'New file', id: 'new-file-link' do - %small - %i.fa.fa-plus + = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do + = @project.path + - tree_breadcrumbs(tree, 6) do |title, path| + %li + - if path + = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path) + - else + = link_to title, '#' + - if allowed_tree_edit? + %li + %span.dropdown + %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"} + = icon('plus') + %ul.dropdown-menu + %li + = link_to namespace_project_new_blob_path(@project.namespace, @project, @id), title: 'Create file', id: 'new-file-link' do + = icon('pencil fw') + Create file + %li + = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do + = icon('file fw') + Upload file + %li.divider + %li + = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do + = icon('folder fw') + New directory -%div#tree-content-holder.tree-content-holder.prepend-top-20 - %table#tree-slider{class: "table_#{@hex_path} tree-table" } - %thead - %tr - %th Name - %th Last Update - %th.hidden-xs - .pull-left Last Commit - .last-commit.hidden-sm.pull-left -   - %i.fa.fa-angle-right -   - %small.light - = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit) - – - = truncate(@commit.title, length: 50) - = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right' +%div#tree-content-holder.tree-content-holder + .tree-table-holder + %table.table#tree-slider{class: "table_#{@hex_path} tree-table table-striped" } + %thead + %tr + %th Name + %th Last Update + %th.hidden-xs + .pull-left Last Commit + .last-commit.hidden-sm.pull-left +   + %i.fa.fa-angle-right +   + %small.light + = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit) + – + = truncate(@commit.title, length: 50) + = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right' - - if @path.present? - %tr.tree-item - %td.tree-item-file-name - = link_to "..", namespace_project_tree_path(@project.namespace, @project, up_dir_path), class: 'prepend-left-10' - %td - %td.hidden-xs + - if @path.present? + %tr.tree-item + %td.tree-item-file-name + = link_to "..", namespace_project_tree_path(@project.namespace, @project, up_dir_path), class: 'prepend-left-10' + %td + %td.hidden-xs - = render_tree(tree) + = render_tree(tree) - if tree.readme = render "projects/tree/readme", readme: tree.readme %div.tree_progress +- if allowed_tree_edit? + = render 'projects/blob/upload', title: 'Upload', placeholder: 'Upload new file', button_title: 'Upload file', form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post + = render 'projects/blob/new_dir' + :javascript // Load last commit log for each file in tree $('#tree-slider').waitForImages(function() { diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 05d754adbe5..261d4a92d7d 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -22,7 +22,7 @@ = f.label :content, class: 'control-label' .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do - = render 'projects/zen', f: f, attr: :content, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :content, classes: 'description form-control js-quick-submit' .col-sm-12.hint .pull-left Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'} .pull-right Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index 03e6a522b25..d179a1abec1 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -3,6 +3,7 @@ = render 'nav' .gray-content-block + = render 'main_links' %h3.page-title All Pages %ul.content-list diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml index 3938c545cad..17b0981f073 100644 --- a/app/views/search/_form.html.haml +++ b/app/views/search/_form.html.haml @@ -6,7 +6,7 @@ .search-holder.clearfix .input-group - = search_field_tag :search, params[:search], placeholder: "Search for projects, issues etc", class: "form-control search-text-input", 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, spellcheck: false %span.input-group-btn = button_tag 'Search', class: "btn btn-primary" - unless params[:snippets].eql? 'true' diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index 5071ff640f1..cc3f1268f8b 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -6,7 +6,7 @@ .max-width-marker = text_area_tag 'commit_message', (params[:commit_message] || local_assigns[:text]), - class: 'form-control', placeholder: local_assigns[:placeholder], + class: 'form-control js-quick-submit', placeholder: local_assigns[:placeholder], required: true, rows: (local_assigns[:rows] || 3) - if local_assigns[:hint] %p.hint diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 33ec726e93c..594e54f404c 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -10,7 +10,7 @@ %strong= 'Title *' .col-sm-10 = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off', - class: 'form-control pad js-gfm-input', required: true + class: 'form-control pad js-gfm-input js-quick-submit', required: true - if issuable.is_a?(MergeRequest) %p.help-block @@ -26,7 +26,7 @@ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render 'projects/zen', f: f, attr: :description, - classes: 'description form-control' + classes: 'description form-control js-quick-submit' .col-sm-12.hint .pull-left Parsed with diff --git a/app/views/shared/issuable/_search_form.html.haml b/app/views/shared/issuable/_search_form.html.haml index 58c3de64b77..3a5ad00aa91 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' } + = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input', spellcheck: false } = 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/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 16e1d8421de..357cfd6a370 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -2,11 +2,12 @@ - avatar = true unless local_assigns[:avatar] == false - stars = true unless local_assigns[:stars] == false - ci = false unless local_assigns[:ci] == true +- skip_namespace = false unless local_assigns[:skip_namespace] == true %ul.projects-list - projects.each_with_index do |project, i| - css_class = (i >= projects_limit) ? 'hide' : nil - = render "shared/projects/project", project: project, + = render "shared/projects/project", project: project, skip_namespace: skip_namespace, avatar: avatar, stars: stars, css_class: css_class, ci: ci - if projects.size > projects_limit diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index e67e5a8a638..aee839b44e7 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -1,6 +1,7 @@ - avatar = true unless local_assigns[:avatar] == false - stars = true unless local_assigns[:stars] == false - ci = false unless local_assigns[:ci] == true +- skip_namespace = false unless local_assigns[:skip_namespace] == true - css_class = '' unless local_assigns[:css_class] - css_class += " no-description" unless project.description.present? %li.project-row{ class: css_class } @@ -11,7 +12,7 @@ = project_icon(project, alt: '', class: 'avatar project-avatar s46') %span.project-full-name %span.namespace-name - - if project.namespace + - if project.namespace && !skip_namespace = project.namespace.human_name \/ %span.project-name.filter-title diff --git a/config/application.rb b/config/application.rb index a96e22211e6..bfa2a809dd7 100644 --- a/config/application.rb +++ b/config/application.rb @@ -74,7 +74,7 @@ module Gitlab origins '*' resource '/api/*', headers: :any, - methods: [:get, :post, :options, :put, :delete], + methods: :any, expose: ['Link'] end end diff --git a/config/mail_room.yml.example b/config/mail_room.yml.example index 82e1a42058e..bb624e8a187 100644 --- a/config/mail_room.yml.example +++ b/config/mail_room.yml.example @@ -9,11 +9,13 @@ # # Whether the IMAP server uses StartTLS # :start_tls: false # # Email account username. Usually the full email address. - # :email: "replies@gitlab.example.com" + # :email: "gitlab-incoming@gmail.com" # # Email account password # :password: "password" + # # The name of the mailbox where incoming mail will end up. Usually "inbox". # :name: "inbox" + # # Always "sidekiq". # :delivery_method: sidekiq # # Always true. @@ -25,5 +27,13 @@ # :namespace: resque:gitlab # # Always "incoming_email". # :queue: incoming_email - # # Always "EmailReceiverWorker" + # # Always "EmailReceiverWorker". # :worker: EmailReceiverWorker + + # # Always "redis". + # :arbitration_method: redis + # :arbitration_options: + # # The URL to the Redis server. Should match the URL in config/resque.yml. + # :redis_url: redis://localhost:6379 + # # Always "mail_room:gitlab". + # :namespace: mail_room:gitlab diff --git a/config/routes.rb b/config/routes.rb index ccce40589e7..8e6fbf6340c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,27 +22,6 @@ Gitlab::Application.routes.draw do get :dumped_yaml end - resources :services, only: [:index, :edit, :update] do - member do - get :test - end - end - - resources :commits, only: [] do - member do - get :status - get :cancel - end - end - - resources :builds, only: [] do - member do - get :cancel - get :status - post :retry - end - end - resources :runner_projects, only: [:create, :destroy] resources :events, only: [:index] @@ -463,6 +442,15 @@ Gitlab::Application.routes.draw do ) end + scope do + post( + '/create_dir/*id', + to: 'tree#create_dir', + constraints: { id: /.+/ }, + as: 'create_dir' + ) + end + scope do get( '/blame/*id', @@ -486,6 +474,7 @@ Gitlab::Application.routes.draw do member do get :branches get :ci + get :cancel_builds end end @@ -590,7 +579,19 @@ Gitlab::Application.routes.draw do end end - resources :builds, only: [:show] + resources :ci_services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do + member do + get :test + end + end + + resources :builds, only: [:show] do + member do + get :cancel + get :status + post :retry + end + end resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do member do diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb index 8f71198e47f..b4639999967 100644 --- a/db/fixtures/development/04_project.rb +++ b/db/fixtures/development/04_project.rb @@ -73,8 +73,13 @@ Sidekiq::Testing.inline! do } project = Projects::CreateService.new(User.first, params).execute + # Seed-Fu runs this entire fixture in a transaction, so the `after_commit` + # hook won't run until after the fixture is loaded. That is too late + # since the Sidekiq::Testing block has already exited. Force clearing + # the `after_commit` queue to ensure the job is run now. + project.send(:_run_after_commit_queue) - if project.valid? + if project.valid? && project.valid_repo? print '.' else puts project.errors.full_messages diff --git a/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb b/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb new file mode 100644 index 00000000000..7f6cd6d5a78 --- /dev/null +++ b/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb @@ -0,0 +1,17 @@ +class NamespacesProjectsPathLowerIndexes < ActiveRecord::Migration + disable_ddl_transaction! + + def up + return unless Gitlab::Database.postgresql? + + execute 'CREATE INDEX CONCURRENTLY index_on_namespaces_lower_path ON namespaces (LOWER(path));' + execute 'CREATE INDEX CONCURRENTLY index_on_projects_lower_path ON projects (LOWER(path));' + end + + def down + return unless Gitlab::Database.postgresql? + + remove_index :namespaces, name: :index_on_namespaces_lower_path + remove_index :projects, name: :index_on_projects_lower_path + end +end diff --git a/db/migrate/20151008123042_add_type_and_description_to_builds.rb b/db/migrate/20151008123042_add_type_and_description_to_builds.rb new file mode 100644 index 00000000000..c72b1c611c6 --- /dev/null +++ b/db/migrate/20151008123042_add_type_and_description_to_builds.rb @@ -0,0 +1,9 @@ +class AddTypeAndDescriptionToBuilds < ActiveRecord::Migration + def change + add_column :ci_builds, :type, :string + add_column :ci_builds, :target_url, :string + add_column :ci_builds, :description, :string + add_index :ci_builds, [:commit_id, :type, :ref] + add_index :ci_builds, [:commit_id, :type, :name, :ref] + end +end diff --git a/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb b/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb new file mode 100644 index 00000000000..f5c44babd84 --- /dev/null +++ b/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb @@ -0,0 +1,5 @@ +class MigrateNameToDescriptionForBuilds < ActiveRecord::Migration + def change + execute("UPDATE ci_builds SET type='Ci::Build' WHERE type IS NULL") + end +end diff --git a/db/schema.rb b/db/schema.rb index 93202f16111..7a11dfca034 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: 20151005162154) do +ActiveRecord::Schema.define(version: 20151008130321) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -103,9 +103,14 @@ ActiveRecord::Schema.define(version: 20151005162154) do t.boolean "tag" t.string "ref" t.integer "user_id" + t.string "type" + t.string "target_url" + t.string "description" end add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree + add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree + add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree add_index "ci_builds", ["project_id", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree diff --git a/doc/api/commits.md b/doc/api/commits.md index eb8d6a43592..9f72adc6ed9 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -62,7 +62,8 @@ Parameters: "authored_date": "2012-09-20T09:06:12+03:00", "parent_ids": [ "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba" - ] + ], + "status": "running" } ``` @@ -156,3 +157,84 @@ Parameters: "line_type": "new" } ``` + +## Get the status of a commit + +Get the statuses of a commit in a project. + +``` +GET /projects/:id/repository/commits/:sha/statuses +``` + +Parameters: + +- `id` (required) - The ID of a project +- `sha` (required) - The commit SHA +- `ref` (optional) - Filter by ref name, it can be branch or tag +- `stage` (optional) - Filter by stage +- `name` (optional) - Filer by status name, eg. jenkins +- `all` (optional) - The flag to return all statuses, not only latest ones + +```json +[ + { + "id": 13, + "sha": "b0b3a907f41409829b307a28b82fdbd552ee5a27", + "ref": "test", + "status": "success", + "name": "ci/jenkins", + "target_url": "http://jenkins/project/url", + "description": "Jenkins success", + "created_at": "2015-10-12T09:47:16.250Z", + "started_at": "2015-10-12T09:47:16.250Z"", + "finished_at": "2015-10-12T09:47:16.262Z", + "author": { + "id": 1, + "username": "admin", + "email": "admin@local.host", + "name": "Administrator", + "blocked": false, + "created_at": "2012-04-29T08:46:00Z" + } + } +] +``` + +## Post the status to commit + +Adds or updates a status of a commit. + +``` +POST /projects/:id/statuses/:sha +``` + +- `id` (required) - The ID of a project +- `sha` (required) - The commit SHA +- `state` (required) - The state of the status. Can be: pending, running, success, failed, canceled +- `ref` (optional) - The ref (branch or tag) to which the status refers +- `name` or `context` (optional) - The label to differentiate this status from the status of other systems. Default: "default" +- `target_url` (optional) - The target URL to associate with this status +- `description` (optional) - The short description of the status + +```json +{ + "id": 13, + "sha": "b0b3a907f41409829b307a28b82fdbd552ee5a27", + "ref": "test", + "status": "success", + "name": "ci/jenkins", + "target_url": "http://jenkins/project/url", + "description": "Jenkins success", + "created_at": "2015-10-12T09:47:16.250Z", + "started_at": "2015-10-12T09:47:16.250Z"", + "finished_at": "2015-10-12T09:47:16.262Z", + "author": { + "id": 1, + "username": "admin", + "email": "admin@local.host", + "name": "Administrator", + "blocked": false, + "created_at": "2012-04-29T08:46:00Z" + } +} +``` diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index bb551fc67f7..ffa7f2cdf14 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -188,6 +188,7 @@ Parameters: - `title` (required) - Title of MR - `description` (optional) - Description of MR - `target_project_id` (optional) - The target project (numeric id) +- `labels` (optional) - Labels for MR as a comma-separated list ```json { @@ -239,6 +240,7 @@ Parameters: - `title` - Title of MR - `description` - Description of MR - `state_event` - New state (close|reopen|merge) +- `labels` (optional) - Labels for MR as a comma-separated list ```json { diff --git a/doc/customization/welcome_message.md b/doc/customization/welcome_message.md index 6c141d1fb7a..e993230bb88 100644 --- a/doc/customization/welcome_message.md +++ b/doc/customization/welcome_message.md @@ -8,31 +8,5 @@ It is possible to add a markdown-formatted welcome message to your GitLab sign-in page. Users of GitLab Enterprise Edition should use the [branded login page feature](/ee/customization/branded_login_page.html) instead. -## Omnibus-gitlab example - -In `/etc/gitlab/gitlab.rb`: - -```ruby -gitlab_rails['extra_sign_in_text'] = <<'EOS' -# ACME GitLab -Welcome to the [ACME](http://www.example.com) GitLab server! -EOS -``` - -Run `sudo gitlab-ctl reconfigure` for changes to take effect. - -## Installation from source - -In `/home/git/gitlab/config/gitlab.yml`: - -```yaml -# snip -production: - # snip - extra: - sign_in_text: | - # ACME GitLab - Welcome to the [ACME](http://www.example.com) GitLab server! -``` - -Run `sudo service gitlab reload` for the change to take effect. +The welcome message (extra_sign_in_text) can now be set/changed in the Admin UI. +Admin area > Settings \ No newline at end of file diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md index 316746ab54d..aafa2345fab 100644 --- a/doc/incoming_email/README.md +++ b/doc/incoming_email/README.md @@ -2,10 +2,6 @@ GitLab can be set up to allow users to comment on issues and merge requests by replying to notification emails. -**Warning**: Do not enable Reply by email if you have **multiple GitLab application servers**. -Due to an issue with the way incoming emails are read from the mail server, every incoming reply-by-email email will result in as many comments being created as you have application servers. -[A fix is being worked on.](https://github.com/tpitale/mail_room/issues/46) - ## Get a mailbox Reply by email requires an IMAP-enabled email account, with a provider or server that supports [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing). Sub-addressing is a feature where any email to `user+some_arbitrary_tag@example.com` will end up in the mailbox for `user@example.com`, and is supported by providers such as Gmail, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix mail server which you can run on-premises. @@ -16,24 +12,35 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ## Set it up -In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`. - ### Omnibus package installations 1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the feature, enter the email address including a placeholder for the `key` that references the item being replied to and fill in the details for your specific IMAP server and email account: ```ruby + # Postfix mail server, assumes mailbox incoming@gitlab.example.com + gitlab_rails['incoming_email_enabled'] = true + gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com" + gitlab_rails['incoming_email_host'] = "gitlab.example.com" # IMAP server host + gitlab_rails['incoming_email_port'] = 143 # IMAP server port + gitlab_rails['incoming_email_ssl'] = false # Whether the IMAP server uses SSL + gitlab_rails['incoming_email_email'] = "incoming" # Email account username. Usually the full email address. + gitlab_rails['incoming_email_password'] = "[REDACTED]" # Email account password + gitlab_rails['incoming_email_mailbox_name'] = "inbox" # The name of the mailbox where incoming mail will end up. Usually "inbox". + ``` + + ```ruby + # Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com gitlab_rails['incoming_email_enabled'] = true gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com" gitlab_rails['incoming_email_host'] = "imap.gmail.com" # IMAP server host gitlab_rails['incoming_email_port'] = 993 # IMAP server port gitlab_rails['incoming_email_ssl'] = true # Whether the IMAP server uses SSL gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com" # Email account username. Usually the full email address. - gitlab_rails['incoming_email_password'] = "password" # Email account password + gitlab_rails['incoming_email_password'] = "[REDACTED]" # Email account password gitlab_rails['incoming_email_mailbox_name'] = "inbox" # The name of the mailbox where incoming mail will end up. Usually "inbox". ``` - As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `gitlab-incoming@gmail.com`. + As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`. 1. Reconfigure GitLab for the changes to take effect: @@ -64,12 +71,20 @@ In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`. ``` ```yaml + # Postfix mail server, assumes mailbox incoming@gitlab.example.com + incoming_email: + enabled: true + address: "incoming+%{key}@gitlab.example.com" + ``` + + ```yaml + # Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com incoming_email: enabled: true address: "gitlab-incoming+%{key}@gmail.com" ``` - As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `gitlab-incoming@gmail.com`. + As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`. 2. Copy `config/mail_room.yml.example` to `config/mail_room.yml`: @@ -84,6 +99,50 @@ In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`. ``` ```yaml + # Postfix mail server + :mailboxes: + - + # IMAP server host + :host: "gitlab.example.com" + # IMAP server port + :port: 143 + # Whether the IMAP server uses SSL + :ssl: false + # Whether the IMAP server uses StartTLS + :start_tls: false + # Email account username. Usually the full email address. + :email: "incoming" + # Email account password + :password: "[REDACTED]" + + # The name of the mailbox where incoming mail will end up. Usually "inbox". + :name: "inbox" + + # Always "sidekiq". + :delivery_method: sidekiq + # Always true. + :delete_after_delivery: true + :delivery_options: + # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml. + :redis_url: redis://localhost:6379 + # Always "resque:gitlab". + :namespace: resque:gitlab + # Always "incoming_email". + :queue: incoming_email + # Always "EmailReceiverWorker" + :worker: EmailReceiverWorker + + # Always "redis". + :arbitration_method: redis + :arbitration_options: + # The URL to the Redis server. Should match the URL in config/resque.yml. + :redis_url: redis://localhost:6379 + # Always "mail_room:gitlab". + :namespace: mail_room:gitlab + ``` + + ```yaml + # Gmail / Google Apps :mailboxes: - # IMAP server host @@ -98,8 +157,10 @@ In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`. :email: "gitlab-incoming@gmail.com" # Email account password :password: "[REDACTED]" + # The name of the mailbox where incoming mail will end up. Usually "inbox". :name: "inbox" + # Always "sidekiq". :delivery_method: sidekiq # Always true. @@ -113,6 +174,14 @@ In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`. :queue: incoming_email # Always "EmailReceiverWorker" :worker: EmailReceiverWorker + + # Always "redis". + :arbitration_method: redis + :arbitration_options: + # The URL to the Redis server. Should match the URL in config/resque.yml. + :redis_url: redis://localhost:6379 + # Always "mail_room:gitlab". + :namespace: mail_room:gitlab ``` 5. Edit the init script configuration at `/etc/default/gitlab` to enable `mail_room`: @@ -143,6 +212,7 @@ In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`. 1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and enter the email address including a placeholder for the `key` that references the item being replied to: ```yaml + # Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com incoming_email: enabled: true address: "gitlab-incoming+%{key}@gmail.com" @@ -159,6 +229,7 @@ In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`. 3. Uncomment the configuration options in `config/mail_room.yml` and fill in the details for your specific IMAP server and email account: ```yaml + # Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com :mailboxes: - # IMAP server host @@ -173,8 +244,10 @@ In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`. :email: "gitlab-incoming@gmail.com" # Email account password :password: "[REDACTED]" + # The name of the mailbox where incoming mail will end up. Usually "inbox". :name: "inbox" + # Always "sidekiq". :delivery_method: sidekiq # Always true. @@ -188,6 +261,14 @@ In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`. :queue: incoming_email # Always "EmailReceiverWorker" :worker: EmailReceiverWorker + + # Always "redis". + :arbitration_method: redis + :arbitration_options: + # The URL to the Redis server. Should match the URL in config/resque.yml. + :redis_url: redis://localhost:6379 + # Always "mail_room:gitlab". + :namespace: mail_room:gitlab ``` 4. Uncomment the `mail_room` line in your `Procfile`: @@ -208,4 +289,4 @@ In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`. bundle exec rake gitlab:incoming_email:check RAILS_ENV=development ``` -8. Reply by email should now be working. \ No newline at end of file +8. Reply by email should now be working. diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md index 4fbd20762da..629d38efc53 100644 --- a/doc/raketasks/user_management.md +++ b/doc/raketasks/user_management.md @@ -56,3 +56,17 @@ bundle exec rake gitlab:import:all_users_to_all_groups RAILS_ENV=production ``` block_auto_created_users: false ``` + +## Disable Two-factor Authentication (2FA) for all users + +This task will disable 2FA for all users that have it enabled. This can be +useful if GitLab's `.secret` file has been lost and users are unable to login, +for example. + +```bash +# omnibus-gitlab +sudo gitlab-rake gitlab:two_factor:disable_for_all_users + +# installation from source +bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production +``` diff --git a/doc/release/monthly.md b/doc/release/monthly.md index c56e99a7005..bd8a67d1d85 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -74,20 +74,19 @@ Xth: (1 working day before the 22nd) - [ ] Update GitLab.com with the latest RC (#LINK) - [ ] Update ci.gitLab.com with the latest RC (#LINK) -22nd before 12AM CET: +22nd before 1200 CET: -Release before 12AM CET / 3AM PST, to make sure the majority of our users +Release before 1200 CET / 2AM 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. - [ ] Merge CE stable into EE stable (#LINK) - [ ] Create the 'x.y.0' tag with the [release tools](https://dev.gitlab.org/gitlab/release-tools) (#LINK) - [ ] Create the 'x.y.0' version on version.gitlab.com -- [ ] Try to do before 11AM CET: Create and push omnibus tags for x.y.0 (will auto-release the packages) (#LINK) -- [ ] Try to do before 12AM CET: Publish the release blog post (#LINK) +- [ ] Try to do before 1100 CET: Create and push omnibus tags for x.y.0 (will auto-release the packages) (#LINK) +- [ ] Try to do before 1200 CET: Publish the release blog post (#LINK) - [ ] Tweet about the release (blog post) (#LINK) -- [ ] Schedule a second tweet of the release announcement with the same text at 6PM CET / 9AM PST - +- [ ] Schedule a second tweet of the release announcement with the same text at 1800 CET / 8AM PST ``` - - - @@ -223,4 +222,4 @@ Consider creating a post on Hacker News. ## Create a WIP blogpost for the next release -Create a WIP blogpost using [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md). \ No newline at end of file +Create a WIP blogpost using [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md). diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index b0e4613cdef..5cb05b13b3e 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -48,16 +48,17 @@ X-Gitlab-Event: System Hook ```json { - "created_at": "2012-07-21T07:30:56Z", - "event_name": "user_add_to_team", - "project_access": "Master", - "project_id": 74, - "project_name": "StoreCloud", - "project_path": "storecloud", - "user_email": "johnsmith@gmail.com", - "user_name": "John Smith", - "user_id": 41, - "project_visibility": "private", + "created_at": "2012-07-21T07:30:56Z", + "event_name": "user_add_to_team", + "project_access": "Master", + "project_id": 74, + "project_name": "StoreCloud", + "project_path": "storecloud", + "project_path_with_namespace": "jsmith/storecloud", + "user_email": "johnsmith@gmail.com", + "user_name": "John Smith", + "user_id": 41, + "project_visibility": "private", } ``` @@ -65,16 +66,17 @@ X-Gitlab-Event: System Hook ```json { - "created_at": "2012-07-21T07:30:56Z", - "event_name": "user_remove_from_team", - "project_access": "Master", - "project_id": 74, - "project_name": "StoreCloud", - "project_path": "storecloud", - "user_email": "johnsmith@gmail.com", - "user_name": "John Smith", - "user_id": 41, - "project_visibility": "private", + "created_at": "2012-07-21T07:30:56Z", + "event_name": "user_remove_from_team", + "project_access": "Master", + "project_id": 74, + "project_name": "StoreCloud", + "project_path": "storecloud", + "project_path_with_namespace": "jsmith/storecloud", + "user_email": "johnsmith@gmail.com", + "user_name": "John Smith", + "user_id": 41, + "project_visibility": "private", } ``` diff --git a/doc/update/7.14-to-8.0.md b/doc/update/7.14-to-8.0.md index 0e65c32098c..7ad4935e839 100644 --- a/doc/update/7.14-to-8.0.md +++ b/doc/update/7.14-to-8.0.md @@ -175,12 +175,21 @@ Also note that because Apache does not support upstreams behind Unix sockets you Now, GitLab CE and EE has CI integrated. However, migrations don't happen automatically and you need to do it manually. Please follow the following guide [to migrate](../migrate_ci_to_ce/README.md) your GitLab CI instance to GitLab CE/EE. -### 10. Start application +### 10. Use Redis v2.4.0+ + +Previous versions of GitLab allowed Redis versions >= 2.0 to be used, but +Sidekiq jobs could fail due to lack of support for the SREM command. GitLab +8.0 now checks that Redis >= 2.4.0 is used. You can check your Redis version +with the following command: + + redis-cli info | grep redis_version + +### 11. Start application sudo service gitlab start sudo service nginx restart -### 11. Check application status +### 12. Check application status Check if GitLab and its environment are configured correctly: diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index a66a863f6c4..da719229ab6 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -23,9 +23,11 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production cd /home/git/gitlab sudo -u git -H git fetch --all sudo -u git -H git checkout -- Gemfile.lock db/schema.rb -LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`) -sudo -u git -H git checkout $LATEST_TAG -b $LATEST_TAG +sudo -u git -H git checkout LATEST_TAG -b LATEST_TAG ``` +Replace `LATEST_TAG` with the latest GitLab tag you want to update to, for example `v8.0.3`. +Use `git tag -l 'v*.[0-9]' --sort='v:refname'` to see a list of all tags. +Make sure to update patch versions only (check your current version with `cat VERSION`) ### 3. Update gitlab-shell to the corresponding version diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index c185ccfcac3..ef99a69f60a 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -314,7 +314,8 @@ X-Gitlab-Event: Note Hook "name": "John Smith", "email": "john@example.com" } - } + }, + "work_in_progress": false } } ``` @@ -500,6 +501,7 @@ X-Gitlab-Event: Merge Request Hook "email": "gitlabdev@dv6700.(none)" } }, + "work_in_progress": false, "url": "http://example.com/diaspora/merge_requests/1", "action": "open" } @@ -537,4 +539,4 @@ When you press 'Test Hook' in GitLab, you should see something like this in the {"before":"077a85dd266e6f3573ef7e9ef8ce3343ad659c4e","after":"95cd4a99e93bc4bbabacfa2cd10e6725b1403c60",} example.com - - [14/May/2014:07:45:26 EDT] "POST / HTTP/1.1" 200 0 - -> / -``` \ No newline at end of file +``` diff --git a/features/dashboard/new_project.feature b/features/dashboard/new_project.feature index bbd82a85e3a..76392068357 100644 --- a/features/dashboard/new_project.feature +++ b/features/dashboard/new_project.feature @@ -7,24 +7,24 @@ Background: And I click "New project" link @javascript - Scenario: I should see New projects page - Then I see "New project" page + Scenario: I should see New Projects page + Then I see "New Project" page Then I see all possible import optios @javascript Scenario: I should see instructions on how to import from Git URL - Given I see "New project" page + Given I see "New Project" page When I click on "Any repo by URL" Then I see instructions on how to import from Git URL @javascript Scenario: I should see instructions on how to import from GitHub - Given I see "New project" page + Given I see "New Project" page When I click on "Import project from GitHub" Then I see instructions on how to import from GitHub @javascript Scenario: I should see Google Code import page - Given I see "New project" page + Given I see "New Project" page When I click on "Google Code" Then I redirected to Google Code import page diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index 58574166ef3..377c5e1a9a7 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -21,12 +21,12 @@ Feature: Project Source Browse Files Then I should see raw file content Scenario: I can create file - Given I click on "new file" link in repo + Given I click on "New file" link in repo Then I can see new file page @javascript Scenario: I can create and commit file - Given I click on "new file" link in repo + Given I click on "New file" link in repo And I edit code And I fill the new file name And I fill the commit message @@ -36,14 +36,13 @@ Feature: Project Source Browse Files @javascript Scenario: I can upload file and commit - Given I click on "new file" link in repo - Then I can see new file page - And I can see "upload an existing one" - And I click on "upload" + Given I click on "Upload file" link in repo And I upload a new text file And I fill the upload file commit message + And I fill the new branch name And I click on "Upload file" Then I can see the new text file + And I am redirected to the uploaded file on new branch And I can see the new commit message @javascript @@ -59,7 +58,7 @@ Feature: Project Source Browse Files @javascript Scenario: I can create and commit file and specify new branch - Given I click on "new file" link in repo + Given I click on "New file" link in repo And I edit code And I fill the new file name And I fill the commit message @@ -83,7 +82,7 @@ Feature: Project Source Browse Files @javascript Scenario: If I enter an illegal file name I see an error message - Given I click on "new file" link in repo + Given I click on "New file" link in repo And I fill the new file name with an illegal name And I edit code And I fill the commit message @@ -138,6 +137,24 @@ Feature: Project Source Browse Files Then I am on the ".gitignore" edit file page And I see a commit error message + @javascript + Scenario: I can create directory in repo + When I click on "New directory" link in repo + And I fill the new directory name + And I fill the commit message + And I fill the new branch name + And I click on "Create directory" + Then I am redirected to the new directory + + @javascript + Scenario: I attempt to create an existing directory + When I click on "New directory" link in repo + And I fill an existing directory name + And I fill the commit message + And I click on "Create directory" + Then I see "Unable to create directory" + And I am redirected to the root directory + @javascript Scenario: I can see editing preview Given I click on ".gitignore" file in repo diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb index 17233f89f38..5a1cc9aa151 100644 --- a/features/steps/admin/projects.rb +++ b/features/steps/admin/projects.rb @@ -41,6 +41,8 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps end step 'I transfer project to group \'Web\'' do + allow_any_instance_of(Projects::TransferService). + to receive(:move_uploads_to_new_namespace).and_return(true) find(:xpath, "//input[@id='new_namespace_id']").set group.id click_button 'Transfer' end diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb index 1e09162a5b5..44a4aa9844a 100644 --- a/features/steps/dashboard/new_project.rb +++ b/features/steps/dashboard/new_project.rb @@ -3,13 +3,13 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps include SharedPaths include SharedProject - step 'I click "New project" link' do + step 'I click "New Project" link' do page.within('.content') do - click_link "New project" + click_link "New Project" end end - step 'I see "New project" page' do + step 'I see "New Project" page' do expect(page).to have_content('Project path') end diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index ae5f90004e6..a3cb83880e3 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -118,6 +118,6 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps step 'I see builds list' do expect(page).to have_content "build: pending" - expect(page).to have_content "Builds for master" + expect(page).to have_content "Latest builds" end end diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index a1a49dd58a6..cb100ca0f54 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -71,7 +71,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I fill the new branch name' do - fill_in :new_branch, with: 'new_branch_name' + fill_in :new_branch, with: 'new_branch_name', visible: true end step 'I fill the new file name with an illegal name' do @@ -90,6 +90,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps click_button 'Commit Changes' end + step 'I click on "Create directory"' do + click_button 'Create directory' + end + step 'I click on "Remove"' do click_button 'Remove' end @@ -110,8 +114,27 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps expect(page).to have_css '.line_holder.new' end - step 'I click on "new file" link in repo' do - click_link 'new-file-link' + step 'I click on "New file" link in repo' do + find('.add-to-tree').click + click_link 'Create file' + end + + step 'I click on "Upload file" link in repo' do + find('.add-to-tree').click + click_link 'Upload file' + end + + step 'I click on "New directory" link in repo' do + find('.add-to-tree').click + click_link 'New directory' + end + + step 'I fill the new directory name' do + fill_in :dir_name, with: new_dir_name + end + + step 'I fill an existing directory name' do + fill_in :dir_name, with: 'files' end step 'I can see new file page' do @@ -119,14 +142,6 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps expect(page).to have_content "Commit message" end - step 'I can see "upload an existing one"' do - expect(page).to have_content "upload an existing one" - end - - step 'I click on "upload"' do - click_link 'upload' - end - step 'I click on "Upload file"' do click_button 'Upload file' end @@ -228,10 +243,30 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps @project.namespace, @project, 'new_branch_name/' + new_file_name)) end + step 'I am redirected to the uploaded file on new branch' do + expect(current_path).to eq(namespace_project_blob_path( + @project.namespace, @project, + 'new_branch_name/' + File.basename(test_text_file))) + end + + step 'I am redirected to the new directory' do + expect(current_path).to eq(namespace_project_tree_path( + @project.namespace, @project, 'new_branch_name/' + new_dir_name)) + end + + step 'I am redirected to the root directory' do + expect(current_path).to eq(namespace_project_tree_path( + @project.namespace, @project, 'master/')) + end + step "I don't see the permalink link" do expect(page).not_to have_link('permalink') end + step 'I see "Unable to create directory"' do + expect(page).to have_content('Directory already exists') + end + step 'I see a commit error message' do expect(page).to have_content('Your changes could not be committed') end @@ -287,6 +322,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps 'not_a_file.md' end + # Constant value that is a valid directory and + # not a directory present at root of the seed repository. + def new_dir_name + 'new_dir/subdir' + end + def drop_in_dropzone(file_path) # Generate a fake input selector page.execute_script <<-JS diff --git a/lib/api/api.rb b/lib/api/api.rb index c09488d3547..afc0402f9e1 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -46,6 +46,7 @@ module API mount Services mount Files mount Commits + mount CommitStatus mount Namespaces mount Branches mount Labels diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb new file mode 100644 index 00000000000..2c0596c9dfb --- /dev/null +++ b/lib/api/commit_statuses.rb @@ -0,0 +1,80 @@ +require 'mime/types' + +module API + # Project commit statuses API + class CommitStatus < Grape::API + resource :projects do + before { authenticate! } + + # Get a commit's statuses + # + # Parameters: + # id (required) - The ID of a project + # sha (required) - The commit hash + # ref (optional) - The ref + # stage (optional) - The stage + # name (optional) - The name + # all (optional) - Show all statuses, default: false + # Examples: + # GET /projects/:id/repository/commits/:sha/statuses + get ':id/repository/commits/:sha/statuses' do + authorize! :read_commit_statuses, user_project + sha = params[:sha] + ci_commit = user_project.ci_commit(sha) + not_found! 'Commit' unless ci_commit + statuses = ci_commit.statuses + statuses = statuses.latest unless parse_boolean(params[:all]) + statuses = statuses.where(ref: params[:ref]) if params[:ref].present? + statuses = statuses.where(stage: params[:stage]) if params[:stage].present? + statuses = statuses.where(name: params[:name]) if params[:name].present? + present paginate(statuses), with: Entities::CommitStatus + end + + # Post status to commit + # + # Parameters: + # id (required) - The ID of a project + # sha (required) - The commit hash + # ref (optional) - The ref + # state (required) - The state of the status. Can be: pending, running, success, error or failure + # target_url (optional) - The target URL to associate with this status + # description (optional) - A short description of the status + # name or context (optional) - A string label to differentiate this status from the status of other systems. Default: "default" + # Examples: + # POST /projects/:id/statuses/:sha + post ':id/statuses/:sha' do + authorize! :create_commit_status, user_project + required_attributes! [:state] + attrs = attributes_for_keys [:ref, :target_url, :description, :context, :name] + commit = @project.commit(params[:sha]) + not_found! 'Commit' unless commit + + ci_commit = @project.ensure_ci_commit(commit.sha) + + name = params[:name] || params[:context] + status = GenericCommitStatus.running_or_pending.find_by(commit: ci_commit, name: name, ref: params[:ref]) + status ||= GenericCommitStatus.new(commit: ci_commit, user: current_user) + status.update(attrs) + + case params[:state].to_s + when 'running' + status.run + when 'success' + status.success + when 'failed' + status.drop + when 'canceled' + status.cancel + else + status.status = params[:state].to_s + end + + if status.save + present status, with: Entities::CommitStatus + else + render_validation_error!(status) + end + end + end + end +end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 9620d36ac41..883a5e14b17 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -149,6 +149,7 @@ module API class RepoCommitDetail < RepoCommit expose :parent_ids, :committed_date, :authored_date + expose :status end class ProjectSnippet < Grape::Entity @@ -228,6 +229,12 @@ module API expose :created_at end + class CommitStatus < Grape::Entity + expose :id, :sha, :ref, :status, :name, :target_url, :description, + :created_at, :started_at, :finished_at + expose :author, using: Entities::UserBasic + end + class Event < Grape::Entity expose :title, :project_id, :action_name expose :target_id, :target_type, :author_id @@ -255,6 +262,18 @@ module API expose :notification_level end + class ProjectService < Grape::Entity + expose :id, :title, :created_at, :updated_at, :active + expose :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events + # Expose serialized properties + expose :properties do |service, options| + field_names = service.fields. + select { |field| options[:include_passwords] || field[:type] != 'password' }. + map { |field| field[:name] } + service.properties.slice(*field_names) + end + end + class ProjectWithAccess < Project expose :permissions do expose :project_access, using: Entities::ProjectAccess do |project, options| diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 63ea2f05438..f3a59fadf24 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -99,7 +99,7 @@ module API # id (required) - The ID of a project - this will be the source of the merge request # source_branch (required) - The source branch # target_branch (required) - The target branch - # target_project - The target project of the merge request defaults to the :id of the project + # target_project_id - The target project of the merge request defaults to the :id of the project # assignee_id - Assignee user ID # title (required) - Title of MR # description - Description of MR diff --git a/lib/api/services.rb b/lib/api/services.rb index 6727e80ac1e..203f04a6259 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -57,7 +57,7 @@ module API # GET /project/:id/services/gitlab-ci # get ':id/services/:service_slug' do - present project_service + present project_service, with: Entities::ProjectService, include_passwords: current_user.is_admin? end end end diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb index f47bc1236b8..b80c0b8b273 100644 --- a/lib/ci/api/entities.rb +++ b/lib/ci/api/entities.rb @@ -2,7 +2,7 @@ module Ci module API module Entities class Commit < Grape::Entity - expose :id, :ref, :sha, :project_id, :before_sha, :created_at + expose :id, :sha, :project_id, :created_at expose :status, :finished_at, :duration expose :git_commit_message, :git_author_name, :git_author_email end @@ -12,7 +12,7 @@ module Ci end class Build < Grape::Entity - expose :id, :commands, :ref, :sha, :project_id, :repo_url, + expose :id, :commands, :ref, :sha, :status, :project_id, :repo_url, :before_sha, :allow_git_fetch, :project_name expose :options do |model| diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb index 322aed5e27c..51e46da82cc 100644 --- a/lib/extracts_path.rb +++ b/lib/extracts_path.rb @@ -110,7 +110,7 @@ module ExtractsPath @project, @ref, @path) rescue RuntimeError, NoMethodError, InvalidPathError - not_found! + render_404 end def tree diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 14ee4701e7b..01b8bda05c6 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -4,7 +4,8 @@ module Gitlab class KeyAdder < Struct.new(:io) def add_key(id, key) - io.puts("#{id}\t#{key.strip}") + key.gsub!(/[[:space:]]+/, ' ').strip! + io.puts("#{id}\t#{key}") end end diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb new file mode 100644 index 00000000000..741a52714ac --- /dev/null +++ b/lib/gitlab/database.rb @@ -0,0 +1,11 @@ +module Gitlab + module Database + def self.mysql? + ActiveRecord::Base.connection.adapter_name.downcase == 'mysql' + end + + def self.postgresql? + ActiveRecord::Base.connection.adapter_name.downcase == 'postgresql' + end + end +end diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index 1ea7751e27d..4be99dd88c2 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -35,7 +35,7 @@ module Gitlab end def find_by_email - ::User.find_by(email: auth_hash.email) + ::User.find_by(email: auth_hash.email.downcase) end def update_user_attributes diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index dc6751fa1c2..d1f22070cba 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -92,12 +92,14 @@ module Gitlab autoload :TableOfContentsFilter, 'gitlab/markdown/table_of_contents_filter' autoload :TaskListFilter, 'gitlab/markdown/task_list_filter' autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter' + autoload :UploadLinkFilter, 'gitlab/markdown/upload_link_filter' def self.gfm_filters @gfm_filters ||= [ Gitlab::Markdown::SyntaxHighlightFilter, Gitlab::Markdown::SanitizationFilter, + Gitlab::Markdown::UploadLinkFilter, Gitlab::Markdown::EmojiFilter, Gitlab::Markdown::TableOfContentsFilter, Gitlab::Markdown::AutolinkFilter, diff --git a/lib/gitlab/markdown/upload_link_filter.rb b/lib/gitlab/markdown/upload_link_filter.rb new file mode 100644 index 00000000000..fbada73ab86 --- /dev/null +++ b/lib/gitlab/markdown/upload_link_filter.rb @@ -0,0 +1,47 @@ +require 'gitlab/markdown' +require 'html/pipeline/filter' +require 'uri' + +module Gitlab + module Markdown + # HTML filter that "fixes" relative upload links to files. + # Context options: + # :project (required) - Current project + # + class UploadLinkFilter < HTML::Pipeline::Filter + def call + doc.search('a').each do |el| + process_link_attr el.attribute('href') + end + + doc.search('img').each do |el| + process_link_attr el.attribute('src') + end + + doc + end + + protected + + def process_link_attr(html_attr) + return if html_attr.blank? + + uri = html_attr.value + if uri.starts_with?("/uploads/") + html_attr.value = build_url(uri).to_s + end + end + + def build_url(uri) + File.join(Gitlab.config.gitlab.url, context[:project].path_with_namespace, uri) + end + + # Ensure that a :project key exists in context + # + # Note that while the key might exist, its value could be nil! + def validate + needs :project + end + end + end +end diff --git a/lib/gitlab/uploads_transfer.rb b/lib/gitlab/uploads_transfer.rb new file mode 100644 index 00000000000..be8fcc7b2d2 --- /dev/null +++ b/lib/gitlab/uploads_transfer.rb @@ -0,0 +1,35 @@ +module Gitlab + class UploadsTransfer + def move_project(project_path, namespace_path_was, namespace_path) + new_namespace_folder = File.join(root_dir, namespace_path) + FileUtils.mkdir_p(new_namespace_folder) unless Dir.exist?(new_namespace_folder) + from = File.join(root_dir, namespace_path_was, project_path) + to = File.join(root_dir, namespace_path, project_path) + move(from, to, "") + end + + def rename_project(path_was, path, namespace_path) + base_dir = File.join(root_dir, namespace_path) + move(path_was, path, base_dir) + end + + def rename_namespace(path_was, path) + move(path_was, path) + end + + private + + def move(path_was, path, base_dir = nil) + base_dir = root_dir unless base_dir + from = File.join(base_dir, path_was) + to = File.join(base_dir, path) + FileUtils.mv(from, to) + rescue Errno::ENOENT + false + end + + def root_dir + File.join(Rails.root, "public", "uploads") + end + end +end diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake index 0ac4b0fa8a3..4cbccf2ca89 100644 --- a/lib/tasks/gitlab/setup.rake +++ b/lib/tasks/gitlab/setup.rake @@ -16,6 +16,7 @@ namespace :gitlab do Rake::Task["db:setup"].invoke Rake::Task["add_limits_mysql"].invoke + Rake::Task["setup_postgresql"].invoke Rake::Task["db:seed_fu"].invoke rescue Gitlab::TaskAbortedByUserError puts "Quitting...".red diff --git a/lib/tasks/gitlab/two_factor.rake b/lib/tasks/gitlab/two_factor.rake new file mode 100644 index 00000000000..9196677a017 --- /dev/null +++ b/lib/tasks/gitlab/two_factor.rake @@ -0,0 +1,23 @@ +namespace :gitlab do + namespace :two_factor do + desc "GitLab | Disable Two-factor authentication (2FA) for all users" + task disable_for_all_users: :environment do + scope = User.with_two_factor + count = scope.count + + if count > 0 + puts "This will disable 2FA for #{count.to_s.red} users..." + + begin + ask_to_continue + scope.find_each(&:disable_two_factor!) + puts "Successfully disabled 2FA for #{count} users.".green + rescue Gitlab::TaskAbortedByUserError + puts "Quitting...".red + end + else + puts "There are currently no users with 2FA enabled.".yellow + end + end + end +end diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake new file mode 100644 index 00000000000..bf6894a8351 --- /dev/null +++ b/lib/tasks/migrate/setup_postgresql.rake @@ -0,0 +1,6 @@ +require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes') + +desc 'GitLab | Sets up PostgreSQL' +task setup_postgresql: :environment do + NamespacesProjectsPathLowerIndexes.new.up +end diff --git a/public/robots.txt b/public/robots.txt index 528f421083e..4f616c7f4c1 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -38,7 +38,8 @@ Disallow: /*/*/edit Disallow: /*/*/raw Disallow: /*/*/blame Disallow: /*/*/commits/*/* -Disallow: /*/*/commit +Disallow: /*/*/commit/*.patch +Disallow: /*/*/commit/*.diff Disallow: /*/*/compare Disallow: /*/*/branches/new Disallow: /*/*/tags/new diff --git a/public/uploads/.gitkeep b/public/uploads/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/spec/benchmarks/finders/trending_projects_finder_spec.rb b/spec/benchmarks/finders/trending_projects_finder_spec.rb new file mode 100644 index 00000000000..551ce21840d --- /dev/null +++ b/spec/benchmarks/finders/trending_projects_finder_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe TrendingProjectsFinder, benchmark: true do + describe '#execute' do + let(:finder) { described_class.new } + let(:user) { create(:user) } + + # to_a is used to force actually running the query (instead of just building + # it). + benchmark_subject { finder.execute(user).non_archived.to_a } + + it { is_expected.to iterate_per_second(500) } + end +end diff --git a/spec/benchmarks/models/project_spec.rb b/spec/benchmarks/models/project_spec.rb new file mode 100644 index 00000000000..cee0949edc5 --- /dev/null +++ b/spec/benchmarks/models/project_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Project, benchmark: true do + describe '.trending' do + let(:group) { create(:group) } + let(:project1) { create(:empty_project, :public, group: group) } + let(:project2) { create(:empty_project, :public, group: group) } + + let(:iterations) { 500 } + + before do + 2.times do + create(:note_on_commit, project: project1) + end + + create(:note_on_commit, project: project2) + end + + describe 'without an explicit start date' do + benchmark_subject { described_class.trending.to_a } + + it { is_expected.to iterate_per_second(iterations) } + end + + describe 'with an explicit start date' do + let(:date) { 1.month.ago } + + benchmark_subject { described_class.trending(date).to_a } + + it { is_expected.to iterate_per_second(iterations) } + end + end + + describe '.find_with_namespace' do + let(:group) { create(:group, name: 'sisinmaru') } + let(:project) { create(:project, name: 'maru', namespace: group) } + + describe 'using a capitalized namespace' do + benchmark_subject { described_class.find_with_namespace('sisinmaru/MARU') } + + it { is_expected.to iterate_per_second(600) } + end + + describe 'using a lowercased namespace' do + benchmark_subject { described_class.find_with_namespace('sisinmaru/maru') } + + it { is_expected.to iterate_per_second(600) } + end + end +end diff --git a/spec/controllers/ci/commits_controller_spec.rb b/spec/controllers/ci/commits_controller_spec.rb deleted file mode 100644 index cc39ce7687c..00000000000 --- a/spec/controllers/ci/commits_controller_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require "spec_helper" - -describe Ci::CommitsController do - describe "GET /status" do - it "returns status of commit" do - commit = FactoryGirl.create :ci_commit - get :status, id: commit.sha, ref_id: commit.ref, project_id: commit.project.id - - expect(response).to be_success - expect(response.code).to eq('200') - JSON.parse(response.body)["status"] == "pending" - end - - it "returns not_found status" do - commit = FactoryGirl.create :ci_commit - get :status, id: commit.sha, ref_id: "deploy", project_id: commit.project.id - - expect(response).to be_success - expect(response.code).to eq('200') - JSON.parse(response.body)["status"] == "not_found" - end - end -end diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb index 53915856357..a474574c6e5 100644 --- a/spec/controllers/projects/tree_controller_spec.rb +++ b/spec/controllers/projects/tree_controller_spec.rb @@ -88,4 +88,40 @@ describe Projects::TreeController do end end end + + describe '#create_dir' do + render_views + + before do + post(:create_dir, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: 'master', + dir_name: path, + new_branch: target_branch, + commit_message: 'Test commit message') + end + + context 'successful creation' do + let(:path) { 'files/new_dir'} + let(:target_branch) { 'master-test'} + + it 'redirects to the new directory' do + expect(subject). + to redirect_to("/#{project.path_with_namespace}/blob/#{target_branch}/#{path}") + expect(flash[:notice]).to eq('The directory has been successfully created') + end + end + + context 'unsuccessful creation' do + let(:path) { 'README.md' } + let(:target_branch) { 'master'} + + it 'does not allow overwriting of existing files' do + expect(subject). + to redirect_to("/#{project.path_with_namespace}/blob/master") + expect(flash[:alert]).to eq('Directory already exists as a file') + end + end + end end diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb index f51abfedae5..93c4494c660 100644 --- a/spec/controllers/projects/uploads_controller_spec.rb +++ b/spec/controllers/projects/uploads_controller_spec.rb @@ -33,7 +33,7 @@ describe Projects::UploadsController do it 'returns a content with original filename, new link, and correct type.' do expect(response.body).to match '\"alt\":\"rails_sample\"' - expect(response.body).to match "\"url\":\"http://localhost/#{project.path_with_namespace}/uploads" + expect(response.body).to match "\"url\":\"/uploads" expect(response.body).to match '\"is_image\":true' end end @@ -49,7 +49,7 @@ describe Projects::UploadsController do it 'returns a content with original filename, new link, and correct type.' do expect(response.body).to match '\"alt\":\"doc_sample.txt\"' - expect(response.body).to match "\"url\":\"http://localhost/#{project.path_with_namespace}/uploads" + expect(response.body).to match "\"url\":\"/uploads" expect(response.body).to match '\"is_image\":false' end end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 21b582afba4..2fcd70182b9 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -27,6 +27,7 @@ FactoryGirl.define do factory :ci_build, class: Ci::Build do + name 'test' ref 'master' tag false started_at 'Di 29. Okt 09:51:28 CET 2013' diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb new file mode 100644 index 00000000000..52de437052d --- /dev/null +++ b/spec/factories/commit_statuses.rb @@ -0,0 +1,15 @@ +FactoryGirl.define do + factory :commit_status, class: CommitStatus do + started_at 'Di 29. Okt 09:51:28 CET 2013' + finished_at 'Di 29. Okt 09:53:28 CET 2013' + name 'default' + status 'success' + description 'commit status' + commit factory: :ci_commit + + factory :generic_commit_status, class: GenericCommitStatus do + name 'generic' + description 'external commit status' + end + end +end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index d0d60491b65..924047a0d8f 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -1,7 +1,6 @@ require 'spec_helper' describe "Builds" do - before do login_as(:user) @commit = FactoryGirl.create :ci_commit @@ -19,4 +18,24 @@ describe "Builds" do it { expect(page).to have_content @commit.git_commit_message } it { expect(page).to have_content @commit.git_author_name } end + + describe "GET /:project/builds/:id/cancel" do + before do + @build.run! + visit cancel_namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + end + + it { expect(page).to have_content 'canceled' } + it { expect(page).to have_content 'Retry' } + end + + describe "POST /:project/builds/:id/retry" do + before do + visit cancel_namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + click_link 'Retry' + end + + it { expect(page).to have_content 'pending' } + it { expect(page).to have_content 'Cancel' } + end end diff --git a/spec/features/ci/builds_spec.rb b/spec/features/ci/builds_spec.rb deleted file mode 100644 index aa0df59c04d..00000000000 --- a/spec/features/ci/builds_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'spec_helper' - -describe "Builds" do - before do - login_as(:user) - @commit = FactoryGirl.create :ci_commit - @build = FactoryGirl.create :ci_build, commit: @commit - @gl_project = @commit.project.gl_project - @gl_project.team << [@user, :master] - end - - describe "GET /:project/builds/:id/cancel" do - before do - @build.run! - visit cancel_ci_project_build_path(@commit.project, @build) - end - - it { expect(page).to have_content 'canceled' } - it { expect(page).to have_content 'Retry' } - end - - describe "POST /:project/builds/:id/retry" do - before do - visit cancel_ci_project_build_path(@commit.project, @build) - click_link 'Retry' - end - - it { expect(page).to have_content 'pending' } - it { expect(page).to have_content 'Cancel' } - end -end diff --git a/spec/features/ci/projects_spec.rb b/spec/features/ci/projects_spec.rb deleted file mode 100644 index c633acf85fb..00000000000 --- a/spec/features/ci/projects_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' - -describe "Projects" do - let(:user) { create(:user) } - - before do - login_as(user) - @project = FactoryGirl.create :ci_project, name: "GitLab / gitlab-shell" - @project.gl_project.team << [user, :master] - end - - describe "GET /ci/projects/:id" do - before do - visit ci_project_path(@project) - end - - it { expect(page).to have_content @project.name } - it { expect(page).to have_content 'All commits' } - end -end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 5da220859e3..1adc2cdf70a 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -12,6 +12,7 @@ describe "Commits" do @ci_project = project.ensure_gitlab_ci_project @commit = FactoryGirl.create :ci_commit, gl_project: project, sha: project.commit.sha @build = FactoryGirl.create :ci_build, commit: @commit + @generic_status = FactoryGirl.create :generic_commit_status, commit: @commit end before do @@ -28,8 +29,16 @@ describe "Commits" do it { expect(page).to have_content @commit.git_author_name } end - describe "Cancel commit" do + describe "Cancel all builds" do it "cancels commit" do + visit ci_status_path(@commit) + click_on "Cancel all" + expect(page).to have_content "canceled" + end + end + + describe "Cancel build" do + it "cancels build" do visit ci_status_path(@commit) click_on "Cancel" expect(page).to have_content "canceled" diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index a362c54b9ad..aac93b17a38 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -50,7 +50,7 @@ feature 'Project', feature: true do end def remove_project - click_link "Remove project" + click_button "Remove project" fill_in 'confirm_name_input', with: project.path click_button 'Confirm' end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index db20b23f87d..b1648055462 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -6,9 +6,11 @@ describe IssuesFinder do let(:project1) { create(:project) } let(:project2) { create(:project) } let(:milestone) { create(:milestone, project: project1) } + let(:label) { create(:label, project: project2) } let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone) } let(:issue2) { create(:issue, author: user, assignee: user, project: project2) } let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) } + let!(:label_link) { create(:label_link, label: label, target: issue2) } before do project1.team << [user, :master] @@ -48,6 +50,24 @@ describe IssuesFinder do expect(issues).to eq([issue1]) end + it 'should filter by no milestone id' do + params = { scope: "all", milestone_title: Milestone::None.title, state: 'opened' } + issues = IssuesFinder.new(user, params).execute + expect(issues).to match_array([issue2, issue3]) + end + + it 'should filter by label name' do + params = { scope: "all", label_name: label.title, state: 'opened' } + issues = IssuesFinder.new(user, params).execute + expect(issues).to eq([issue2]) + end + + it 'should filter by no label name' do + params = { scope: "all", label_name: Label::None.title, state: 'opened' } + issues = IssuesFinder.new(user, params).execute + expect(issues).to match_array([issue1, issue3]) + end + it 'should be empty for unauthorized user' do params = { scope: "all", state: 'opened' } issues = IssuesFinder.new(nil, params).execute diff --git a/spec/finders/trending_projects_finder_spec.rb b/spec/finders/trending_projects_finder_spec.rb new file mode 100644 index 00000000000..a49cbfd5160 --- /dev/null +++ b/spec/finders/trending_projects_finder_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe TrendingProjectsFinder do + let(:user) { build(:user) } + + describe '#execute' do + describe 'without an explicit start date' do + subject { described_class.new } + + it 'returns the trending projects' do + relation = double(:ar_relation) + + allow(subject).to receive(:projects_for) + .with(user) + .and_return(relation) + + allow(relation).to receive(:trending) + .with(an_instance_of(ActiveSupport::TimeWithZone)) + end + end + + describe 'with an explicit start date' do + let(:date) { 2.months.ago } + + subject { described_class.new } + + it 'returns the trending projects' do + relation = double(:ar_relation) + + allow(subject).to receive(:projects_for) + .with(user) + .and_return(relation) + + allow(relation).to receive(:trending) + .with(date) + end + end + end +end diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 0c8d06b7059..fb70a36dc02 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -14,6 +14,11 @@ describe LabelsHelper do expect(label).not_to receive(:project) link_to_label(label) end + + it 'includes option for "No Label"' do + result = project_labels_options(project) + expect(result).to include('No Label') + end end context 'without @project set' do diff --git a/spec/javascripts/behaviors/quick_submit_spec.js.coffee b/spec/javascripts/behaviors/quick_submit_spec.js.coffee new file mode 100644 index 00000000000..09708c12ed4 --- /dev/null +++ b/spec/javascripts/behaviors/quick_submit_spec.js.coffee @@ -0,0 +1,70 @@ +#= require behaviors/quick_submit + +describe 'Quick Submit behavior', -> + fixture.preload('behaviors/quick_submit.html') + + beforeEach -> + fixture.load('behaviors/quick_submit.html') + + # Prevent a form submit from moving us off the testing page + $('form').submit (e) -> e.preventDefault() + + @spies = { + submit: spyOnEvent('form', 'submit') + } + + it 'does not respond to other keyCodes', -> + $('input').trigger(keydownEvent(keyCode: 32)) + + expect(@spies.submit).not.toHaveBeenTriggered() + + it 'does not respond to Enter alone', -> + $('input').trigger(keydownEvent(ctrlKey: false, metaKey: false)) + + expect(@spies.submit).not.toHaveBeenTriggered() + + it 'does not respond to repeated events', -> + $('input').trigger(keydownEvent(repeat: true)) + + expect(@spies.submit).not.toHaveBeenTriggered() + + it 'disables submit buttons', -> + $('textarea').trigger(keydownEvent()) + + expect($('input[type=submit]')).toBeDisabled() + expect($('button[type=submit]')).toBeDisabled() + + # We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll + # only run the tests that apply to the current platform + if navigator.userAgent.match(/Macintosh/) + it 'responds to Meta+Enter', -> + $('input').trigger(keydownEvent()) + + expect(@spies.submit).toHaveBeenTriggered() + + it 'excludes other modifier keys', -> + $('input').trigger(keydownEvent(altKey: true)) + $('input').trigger(keydownEvent(ctrlKey: true)) + $('input').trigger(keydownEvent(shiftKey: true)) + + expect(@spies.submit).not.toHaveBeenTriggered() + else + it 'responds to Ctrl+Enter', -> + $('input').trigger(keydownEvent()) + + expect(@spies.submit).toHaveBeenTriggered() + + it 'excludes other modifier keys', -> + $('input').trigger(keydownEvent(altKey: true)) + $('input').trigger(keydownEvent(metaKey: true)) + $('input').trigger(keydownEvent(shiftKey: true)) + + expect(@spies.submit).not.toHaveBeenTriggered() + + keydownEvent = (options) -> + if navigator.userAgent.match(/Macintosh/) + defaults = { keyCode: 13, metaKey: true } + else + defaults = { keyCode: 13, ctrlKey: true } + + $.Event('keydown', $.extend({}, defaults, options)) diff --git a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml new file mode 100644 index 00000000000..b80a28a33ea --- /dev/null +++ b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml @@ -0,0 +1,6 @@ +%form{ action: '/foo' } + %input.js-quick-submit{ type: 'text' } + %textarea.js-quick-submit + + %input{ type: 'submit'} Submit + %button.btn{ type: 'submit' } Submit diff --git a/spec/javascripts/fixtures/line_highlighter.html.haml b/spec/javascripts/fixtures/line_highlighter.html.haml index da1ebcdb23c..514877340e4 100644 --- a/spec/javascripts/fixtures/line_highlighter.html.haml +++ b/spec/javascripts/fixtures/line_highlighter.html.haml @@ -1,4 +1,4 @@ -#tree-content-holder +#blob-content-holder .file-content .line-numbers - 1.upto(25) do |i| diff --git a/spec/javascripts/line_highlighter_spec.js.coffee b/spec/javascripts/line_highlighter_spec.js.coffee index 57453c716a5..a073f21e7bc 100644 --- a/spec/javascripts/line_highlighter_spec.js.coffee +++ b/spec/javascripts/line_highlighter_spec.js.coffee @@ -39,7 +39,7 @@ describe 'LineHighlighter', -> expect(spy).toHaveBeenPrevented() it 'handles garbage input from the hash', -> - func = -> new LineHighlighter('#tree-content-holder') + func = -> new LineHighlighter('#blob-content-holder') expect(func).not.toThrow() describe '#clickHandler', -> diff --git a/spec/javascripts/spec_helper.coffee b/spec/javascripts/spec_helper.coffee index 47b41dd2c81..90b02a6aec5 100644 --- a/spec/javascripts/spec_helper.coffee +++ b/spec/javascripts/spec_helper.coffee @@ -9,6 +9,7 @@ # require the specific files that are being used in the spec that tests them. #= require jquery +#= require jquery.turbolinks #= require bootstrap #= require underscore diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/backend/shell_spec.rb index b6d04330599..b60e23454d6 100644 --- a/spec/lib/gitlab/backend/shell_spec.rb +++ b/spec/lib/gitlab/backend/shell_spec.rb @@ -15,4 +15,17 @@ describe Gitlab::Shell do it { is_expected.to respond_to :fork_repository } it { expect(gitlab_shell.url_to_repo('diaspora')).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git") } + + describe Gitlab::Shell::KeyAdder do + describe '#add_key' do + it 'normalizes space characters in the key' do + io = spy + adder = described_class.new(io) + + adder.add_key('key-42', "sha-rsa foo\tbar\tbaz") + + expect(io).to have_received(:puts).with("key-42\tsha-rsa foo bar baz") + end + end + end end diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb new file mode 100644 index 00000000000..7cdebdf209a --- /dev/null +++ b/spec/lib/gitlab/database_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::Database do + # These are just simple smoke tests to check if the methods work (regardless + # of what they may return). + describe '.mysql?' do + subject { described_class.mysql? } + + it { is_expected.to satisfy { |val| val == true || val == false } } + end + + describe '.postgresql?' do + subject { described_class.postgresql? } + + it { is_expected.to satisfy { |val| val == true || val == false } } + end +end diff --git a/spec/lib/gitlab/email/attachment_uploader_spec.rb b/spec/lib/gitlab/email/attachment_uploader_spec.rb index e8208e15e29..8fb432367b6 100644 --- a/spec/lib/gitlab/email/attachment_uploader_spec.rb +++ b/spec/lib/gitlab/email/attachment_uploader_spec.rb @@ -13,7 +13,6 @@ describe Gitlab::Email::AttachmentUploader do expect(link).not_to be_nil expect(link[:is_image]).to be_truthy expect(link[:alt]).to eq("bricks") - expect(link[:url]).to include("/#{project.path_with_namespace}") expect(link[:url]).to include("bricks.png") end end diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index fd2e5f6d0e1..b5b56a34952 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -13,6 +13,17 @@ describe Gitlab::LDAP::User do let(:auth_hash) do OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info) end + let(:ldap_user_upper_case) { Gitlab::LDAP::User.new(auth_hash_upper_case) } + let(:info_upper_case) do + { + name: 'John', + email: 'John@Example.com', # Email address has upper case chars + nickname: 'john' + } + end + let(:auth_hash_upper_case) do + OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info_upper_case) + end describe :changed? do it "marks existing ldap user as changed" do @@ -57,6 +68,16 @@ describe Gitlab::LDAP::User do expect(existing_user.id).to eql ldap_user.gl_user.id end + it 'connects to existing ldap user if the extern_uid changes and email address has upper case characters' do + existing_user = create(:omniauth_user, email: 'john@example.com', extern_uid: 'old-uid', provider: 'ldapmain') + expect{ ldap_user_upper_case.save }.not_to change{ User.count } + + existing_user.reload + expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid' + expect(existing_user.ldap_identity.provider).to eql 'ldapmain' + expect(existing_user.id).to eql ldap_user.gl_user.id + end + it 'maintains an identity per provider' do existing_user = create(:omniauth_user, email: 'john@example.com', provider: 'twitter') expect(existing_user.identities.count).to eql(1) diff --git a/spec/lib/gitlab/markdown/upload_link_filter_spec.rb b/spec/lib/gitlab/markdown/upload_link_filter_spec.rb new file mode 100644 index 00000000000..9ae45a6f559 --- /dev/null +++ b/spec/lib/gitlab/markdown/upload_link_filter_spec.rb @@ -0,0 +1,75 @@ +# encoding: UTF-8 + +require 'spec_helper' + +module Gitlab::Markdown + describe UploadLinkFilter do + def filter(doc, contexts = {}) + contexts.reverse_merge!({ + project: project + }) + + described_class.call(doc, contexts) + end + + def image(path) + %() + end + + def link(path) + %(#{path}) + end + + let(:project) { create(:project) } + + shared_examples :preserve_unchanged do + it 'does not modify any relative URL in anchor' do + doc = filter(link('README.md')) + expect(doc.at_css('a')['href']).to eq 'README.md' + end + + it 'does not modify any relative URL in image' do + doc = filter(image('files/images/logo-black.png')) + expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png' + end + end + + it 'does not raise an exception on invalid URIs' do + act = link("://foo") + expect { filter(act) }.not_to raise_error + end + + context 'with a valid repository' do + it 'rebuilds relative URL for a link' do + doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) + expect(doc.at_css('a')['href']). + to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + end + + it 'rebuilds relative URL for an image' do + doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) + expect(doc.at_css('a')['href']). + to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + end + + it 'does not modify absolute URL' do + doc = filter(link('http://example.com')) + expect(doc.at_css('a')['href']).to eq 'http://example.com' + end + + it 'supports Unicode filenames' do + path = '/uploads/한글.png' + escaped = Addressable::URI.escape(path) + + # Stub these methods so the file doesn't actually need to be in the repo + allow_any_instance_of(described_class). + to receive(:file_exists?).and_return(true) + allow_any_instance_of(described_class). + to receive(:image?).with(path).and_return(true) + + doc = filter(image(escaped)) + expect(doc.at_css('img')['src']).to match "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/%ED%95%9C%EA%B8%80.png" + end + end + end +end diff --git a/spec/lib/gitlab/uploads_transfer_spec.rb b/spec/lib/gitlab/uploads_transfer_spec.rb new file mode 100644 index 00000000000..260364a513e --- /dev/null +++ b/spec/lib/gitlab/uploads_transfer_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Gitlab::UploadsTransfer do + before do + @root_dir = File.join(Rails.root, "public", "uploads") + @upload_transfer = Gitlab::UploadsTransfer.new + + @project_path_was = "test_project_was" + @project_path = "test_project" + @namespace_path_was = "test_namespace_was" + @namespace_path = "test_namespace" + end + + after do + FileUtils.rm_rf([ + File.join(@root_dir, @namespace_path), + File.join(@root_dir, @namespace_path_was) + ]) + end + + describe '#move_project' do + it "moves project upload to another namespace" do + FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path)) + @upload_transfer.move_project(@project_path, @namespace_path_was, @namespace_path) + + expected_path = File.join(@root_dir, @namespace_path, @project_path) + expect(Dir.exist?(expected_path)).to be_truthy + end + end + + describe '#rename_project' do + it "renames project" do + FileUtils.mkdir_p(File.join(@root_dir, @namespace_path, @project_path_was)) + @upload_transfer.rename_project(@project_path_was, @project_path, @namespace_path) + + expected_path = File.join(@root_dir, @namespace_path, @project_path) + expect(Dir.exist?(expected_path)).to be_truthy + end + end + + describe '#rename_namespace' do + it "renames namespace" do + FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path)) + @upload_transfer.rename_namespace(@namespace_path_was, @namespace_path) + + expected_path = File.join(@root_dir, @namespace_path, @project_path) + expect(Dir.exist?(expected_path)).to be_truthy + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/build_spec.rb similarity index 74% rename from spec/models/ci/build_spec.rb rename to spec/models/build_spec.rb index da56f6e31ae..d875015b991 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/build_spec.rb @@ -30,17 +30,9 @@ describe Ci::Build do let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project } let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } let(:build) { FactoryGirl.create :ci_build, commit: commit } - subject { build } - it { is_expected.to belong_to(:commit) } - it { is_expected.to belong_to(:user) } - it { is_expected.to validate_presence_of :status } it { is_expected.to validate_presence_of :ref } - it { is_expected.to respond_to :success? } - it { is_expected.to respond_to :failed? } - it { is_expected.to respond_to :running? } - it { is_expected.to respond_to :pending? } it { is_expected.to respond_to :trace_html } describe :first_pending do @@ -67,72 +59,6 @@ describe Ci::Build do end end - describe :started? do - subject { build.started? } - - context 'without started_at' do - before { build.started_at = nil } - - it { is_expected.to be_falsey } - end - - %w(running success failed).each do |status| - context "if build status is #{status}" do - before { build.status = status } - - it { is_expected.to be_truthy } - end - end - - %w(pending canceled).each do |status| - context "if build status is #{status}" do - before { build.status = status } - - it { is_expected.to be_falsey } - end - end - end - - describe :active? do - subject { build.active? } - - %w(pending running).each do |state| - context "if build.status is #{state}" do - before { build.status = state } - - it { is_expected.to be_truthy } - end - end - - %w(success failed canceled).each do |state| - context "if build.status is #{state}" do - before { build.status = state } - - it { is_expected.to be_falsey } - end - end - end - - describe :complete? do - subject { build.complete? } - - %w(success failed canceled).each do |state| - context "if build.status is #{state}" do - before { build.status = state } - - it { is_expected.to be_truthy } - end - end - - %w(pending running).each do |state| - context "if build.status is #{state}" do - before { build.status = state } - - it { is_expected.to be_falsey } - end - end - end - describe :ignored? do subject { build.ignored? } @@ -200,31 +126,6 @@ describe Ci::Build do it { is_expected.to eq(commit.project.timeout) } end - describe :duration do - subject { build.duration } - - it { is_expected.to eq(120.0) } - - context 'if the building process has not started yet' do - before do - build.started_at = nil - build.finished_at = nil - end - - it { is_expected.to be_nil } - end - - context 'if the building process has started' do - before do - build.started_at = Time.now - 1.minute - build.finished_at = nil - end - - it { is_expected.to be_a(Float) } - it { is_expected.to be > 0.0 } - end - end - describe :options do let(:options) do { @@ -239,18 +140,6 @@ describe Ci::Build do it { is_expected.to eq(options) } end - describe :sha do - subject { build.sha } - - it { is_expected.to eq(commit.sha) } - end - - describe :short_sha do - subject { build.short_sha } - - it { is_expected.to eq(commit.short_sha) } - end - describe :allow_git_fetch do subject { build.allow_git_fetch } diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index acff1ddf0fc..330971174fb 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -23,6 +23,8 @@ describe Ci::Commit do let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } it { is_expected.to belong_to(:gl_project) } + it { is_expected.to have_many(:statuses) } + it { is_expected.to have_many(:trigger_requests) } it { is_expected.to have_many(:builds) } it { is_expected.to validate_presence_of :sha } @@ -47,10 +49,12 @@ describe Ci::Commit do @second = FactoryGirl.create :ci_build, commit: commit end - it "creates new build" do + it "creates only a new build" do expect(commit.builds.count(:all)).to eq 2 + expect(commit.statuses.count(:all)).to eq 2 commit.retry expect(commit.builds.count(:all)).to eq 3 + expect(commit.statuses.count(:all)).to eq 3 end end @@ -78,8 +82,8 @@ describe Ci::Commit do subject { commit.stage } before do - @second = FactoryGirl.create :ci_build, commit: commit, name: 'deploy', stage: 'deploy', stage_idx: 1, status: :pending - @first = FactoryGirl.create :ci_build, commit: commit, name: 'test', stage: 'test', stage_idx: 0, status: :pending + @second = FactoryGirl.create :commit_status, commit: commit, name: 'deploy', stage: 'deploy', stage_idx: 1, status: 'pending' + @first = FactoryGirl.create :commit_status, commit: commit, name: 'test', stage: 'test', stage_idx: 0, status: 'pending' end it 'returns first running stage' do @@ -88,7 +92,7 @@ describe Ci::Commit do context 'first build succeeded' do before do - @first.update_attributes(status: :success) + @first.success end it 'returns last running stage' do @@ -98,8 +102,8 @@ describe Ci::Commit do context 'all builds succeeded' do before do - @first.update_attributes(status: :success) - @second.update_attributes(status: :success) + @first.success + @second.success end it 'returns nil' do @@ -111,6 +115,33 @@ describe Ci::Commit do describe :create_next_builds do end + describe :refs do + subject { commit.refs } + + before do + FactoryGirl.create :commit_status, commit: commit, name: 'deploy' + FactoryGirl.create :commit_status, commit: commit, name: 'deploy', ref: 'develop' + FactoryGirl.create :commit_status, commit: commit, name: 'deploy', ref: 'master' + end + + it 'returns all refs' do + is_expected.to contain_exactly('master', 'develop', nil) + end + end + + describe :retried do + subject { commit.retried } + + before do + @commit1 = FactoryGirl.create :ci_build, commit: commit, name: 'deploy' + @commit2 = FactoryGirl.create :ci_build, commit: commit, name: 'deploy' + end + + it 'returns old builds' do + is_expected.to contain_exactly(@commit1) + end + end + describe :create_builds do let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } @@ -194,9 +225,10 @@ describe Ci::Commit do it 'rebuilds commit' do expect(commit.status).to eq('skipped') expect(create_builds(trigger_request)).to be_truthy - commit.builds.reload - expect(commit.builds.size).to eq(2) - expect(commit.status).to eq('pending') + + # since everything in Ci::Commit is cached we need to fetch a new object + new_commit = Ci::Commit.find_by_id(commit.id) + expect(new_commit.status).to eq('pending') end end end @@ -252,10 +284,10 @@ describe Ci::Commit do describe :should_create_next_builds? do before do - @build1 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'master', tag: false, status: :success - @build2 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'develop', tag: false, status: :failed - @build3 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'master', tag: true, status: :failed - @build4 = FactoryGirl.create :ci_build, commit: commit, name: 'build4', ref: 'master', tag: false, status: :success + @build1 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'master', tag: false, status: 'success' + @build2 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'develop', tag: false, status: 'failed' + @build3 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'master', tag: true, status: 'failed' + @build4 = FactoryGirl.create :ci_build, commit: commit, name: 'build4', ref: 'master', tag: false, status: 'success' end context 'for success' do @@ -266,7 +298,7 @@ describe Ci::Commit do context 'for failed' do before do - @build4.update_attributes(status: :failed) + @build4.update_attributes(status: 'failed') end it 'to not create' do @@ -286,7 +318,7 @@ describe Ci::Commit do context 'for running' do before do - @build4.update_attributes(status: :running) + @build4.update_attributes(status: 'running') end it 'to not create' do @@ -296,7 +328,7 @@ describe Ci::Commit do context 'for retried' do before do - @build5 = FactoryGirl.create :ci_build, commit: commit, name: 'build4', ref: 'master', tag: false, status: :failed + @build5 = FactoryGirl.create :ci_build, commit: commit, name: 'build4', ref: 'master', tag: false, status: 'failed' end it 'to not create' do diff --git a/spec/models/ci/project_services/mail_service_spec.rb b/spec/models/ci/project_services/mail_service_spec.rb index 04e870dce7f..d9b3d34ff15 100644 --- a/spec/models/ci/project_services/mail_service_spec.rb +++ b/spec/models/ci/project_services/mail_service_spec.rb @@ -35,7 +35,7 @@ describe Ci::MailService do let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true) } let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit, user: user) } + let(:build) { FactoryGirl.create(:ci_build, status: 'failed', commit: commit, user: user) } before do allow(mail).to receive_messages( @@ -58,7 +58,7 @@ describe Ci::MailService do let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true, email_only_broken_builds: false) } let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) } + let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) } before do allow(mail).to receive_messages( @@ -86,7 +86,7 @@ describe Ci::MailService do end let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) } + let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) } before do allow(mail).to receive_messages( @@ -115,7 +115,7 @@ describe Ci::MailService do end let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) } + let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) } before do allow(mail).to receive_messages( @@ -144,7 +144,7 @@ describe Ci::MailService do end let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) } + let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) } before do allow(mail).to receive_messages( @@ -167,7 +167,7 @@ describe Ci::MailService do end let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) } let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) } - let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit, user: user) } + let(:build) { FactoryGirl.create(:ci_build, status: 'failed', commit: commit, user: user) } before do allow(mail).to receive_messages( diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb new file mode 100644 index 00000000000..c96a606fdaa --- /dev/null +++ b/spec/models/commit_status_spec.rb @@ -0,0 +1,164 @@ +require 'spec_helper' + +describe CommitStatus do + let(:commit) { FactoryGirl.create :ci_commit } + let(:commit_status) { FactoryGirl.create :commit_status, commit: commit } + + it { is_expected.to belong_to(:commit) } + it { is_expected.to belong_to(:user) } + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_inclusion_of(:status).in_array(%w(pending running failed success canceled)) } + + it { is_expected.to delegate_method(:sha).to(:commit) } + it { is_expected.to delegate_method(:short_sha).to(:commit) } + it { is_expected.to delegate_method(:gl_project).to(:commit) } + + it { is_expected.to respond_to :success? } + it { is_expected.to respond_to :failed? } + it { is_expected.to respond_to :running? } + it { is_expected.to respond_to :pending? } + + describe :author do + subject { commit_status.author } + before { commit_status.author = User.new } + + it { is_expected.to eq(commit_status.user) } + end + + describe :started? do + subject { commit_status.started? } + + context 'without started_at' do + before { commit_status.started_at = nil } + + it { is_expected.to be_falsey } + end + + %w(running success failed).each do |status| + context "if commit status is #{status}" do + before { commit_status.status = status } + + it { is_expected.to be_truthy } + end + end + + %w(pending canceled).each do |status| + context "if commit status is #{status}" do + before { commit_status.status = status } + + it { is_expected.to be_falsey } + end + end + end + + describe :active? do + subject { commit_status.active? } + + %w(pending running).each do |state| + context "if commit_status.status is #{state}" do + before { commit_status.status = state } + + it { is_expected.to be_truthy } + end + end + + %w(success failed canceled).each do |state| + context "if commit_status.status is #{state}" do + before { commit_status.status = state } + + it { is_expected.to be_falsey } + end + end + end + + describe :complete? do + subject { commit_status.complete? } + + %w(success failed canceled).each do |state| + context "if commit_status.status is #{state}" do + before { commit_status.status = state } + + it { is_expected.to be_truthy } + end + end + + %w(pending running).each do |state| + context "if commit_status.status is #{state}" do + before { commit_status.status = state } + + it { is_expected.to be_falsey } + end + end + end + + describe :duration do + subject { commit_status.duration } + + it { is_expected.to eq(120.0) } + + context 'if the building process has not started yet' do + before do + commit_status.started_at = nil + commit_status.finished_at = nil + end + + it { is_expected.to be_nil } + end + + context 'if the building process has started' do + before do + commit_status.started_at = Time.now - 1.minute + commit_status.finished_at = nil + end + + it { is_expected.to be_a(Float) } + it { is_expected.to be > 0.0 } + end + end + + describe :latest do + subject { CommitStatus.latest.order(:id) } + + before do + @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running' + @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending' + @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'cc', status: 'success' + @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'bb', status: 'success' + @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'success' + end + + it 'return unique statuses' do + is_expected.to eq([@commit2, @commit3, @commit4, @commit5]) + end + end + + describe :for_ref do + subject { CommitStatus.for_ref('bb').order(:id) } + + before do + @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running' + @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending' + @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: nil, status: 'success' + end + + it 'return statuses with equal and nil ref set' do + is_expected.to eq([@commit1]) + end + end + + describe :running_or_pending do + subject { CommitStatus.running_or_pending.order(:id) } + + before do + @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running' + @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending' + @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: nil, status: 'success' + @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'dd', ref: nil, status: 'failed' + @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'ee', ref: nil, status: 'canceled' + end + + it 'return statuses that are running or pending' do + is_expected.to eq([@commit1, @commit2]) + end + end +end diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb new file mode 100644 index 00000000000..f7ed30f8198 --- /dev/null +++ b/spec/models/concerns/case_sensitivity_spec.rb @@ -0,0 +1,189 @@ +require 'spec_helper' + +describe CaseSensitivity do + describe '.iwhere' do + let(:connection) { ActiveRecord::Base.connection } + let(:model) { Class.new { include CaseSensitivity } } + + describe 'using PostgreSQL' do + before do + allow(Gitlab::Database).to receive(:postgresql?).and_return(true) + allow(Gitlab::Database).to receive(:mysql?).and_return(false) + end + + describe 'with a single column/value pair' do + it 'returns the criteria for a column and a value' do + criteria = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:foo). + and_return('"foo"') + + expect(model).to receive(:where). + with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar'). + and_return(criteria) + + expect(model.iwhere(foo: 'bar')).to eq(criteria) + end + + it 'returns the criteria for a column with a table, and a value' do + criteria = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:'foo.bar'). + and_return('"foo"."bar"') + + expect(model).to receive(:where). + with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar'). + and_return(criteria) + + expect(model.iwhere(:'foo.bar' => 'bar')).to eq(criteria) + end + end + + describe 'with multiple column/value pairs' do + it 'returns the criteria for a column and a value' do + initial = double(:criteria) + final = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:foo). + and_return('"foo"') + + expect(connection).to receive(:quote_table_name). + with(:bar). + and_return('"bar"') + + expect(model).to receive(:where). + with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar'). + and_return(initial) + + expect(initial).to receive(:where). + with(%q{LOWER("bar") = LOWER(:value)}, value: 'baz'). + and_return(final) + + got = model.iwhere(foo: 'bar', bar: 'baz') + + expect(got).to eq(final) + end + + it 'returns the criteria for a column with a table, and a value' do + initial = double(:criteria) + final = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:'foo.bar'). + and_return('"foo"."bar"') + + expect(connection).to receive(:quote_table_name). + with(:'foo.baz'). + and_return('"foo"."baz"') + + expect(model).to receive(:where). + with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar'). + and_return(initial) + + expect(initial).to receive(:where). + with(%q{LOWER("foo"."baz") = LOWER(:value)}, value: 'baz'). + and_return(final) + + got = model.iwhere(:'foo.bar' => 'bar', + :'foo.baz' => 'baz') + + expect(got).to eq(final) + end + end + end + + describe 'using MySQL' do + before do + allow(Gitlab::Database).to receive(:postgresql?).and_return(false) + allow(Gitlab::Database).to receive(:mysql?).and_return(true) + end + + describe 'with a single column/value pair' do + it 'returns the criteria for a column and a value' do + criteria = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:foo). + and_return('`foo`') + + expect(model).to receive(:where). + with(%q{`foo` = :value}, value: 'bar'). + and_return(criteria) + + expect(model.iwhere(foo: 'bar')).to eq(criteria) + end + + it 'returns the criteria for a column with a table, and a value' do + criteria = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:'foo.bar'). + and_return('`foo`.`bar`') + + expect(model).to receive(:where). + with(%q{`foo`.`bar` = :value}, value: 'bar'). + and_return(criteria) + + expect(model.iwhere(:'foo.bar' => 'bar')). + to eq(criteria) + end + end + + describe 'with multiple column/value pairs' do + it 'returns the criteria for a column and a value' do + initial = double(:criteria) + final = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:foo). + and_return('`foo`') + + expect(connection).to receive(:quote_table_name). + with(:bar). + and_return('`bar`') + + expect(model).to receive(:where). + with(%q{`foo` = :value}, value: 'bar'). + and_return(initial) + + expect(initial).to receive(:where). + with(%q{`bar` = :value}, value: 'baz'). + and_return(final) + + got = model.iwhere(foo: 'bar', bar: 'baz') + + expect(got).to eq(final) + end + + it 'returns the criteria for a column with a table, and a value' do + initial = double(:criteria) + final = double(:criteria) + + expect(connection).to receive(:quote_table_name). + with(:'foo.bar'). + and_return('`foo`.`bar`') + + expect(connection).to receive(:quote_table_name). + with(:'foo.baz'). + and_return('`foo`.`baz`') + + expect(model).to receive(:where). + with(%q{`foo`.`bar` = :value}, value: 'bar'). + and_return(initial) + + expect(initial).to receive(:where). + with(%q{`foo`.`baz` = :value}, value: 'baz'). + and_return(final) + + got = model.iwhere(:'foo.bar' => 'bar', + :'foo.baz' => 'baz') + + expect(got).to eq(final) + end + end + end + end +end diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb new file mode 100644 index 00000000000..f442fa5fbe5 --- /dev/null +++ b/spec/models/generic_commit_status_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe GenericCommitStatus do + let(:commit) { FactoryGirl.create :ci_commit } + let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, commit: commit } + + describe :context do + subject { generic_commit_status.context } + before { generic_commit_status.context = 'my_context' } + + it { is_expected.to eq(generic_commit_status.name) } + end + + describe :tags do + subject { generic_commit_status.tags } + + it { is_expected.to eq([:external]) } + end + + describe :set_default_values do + before do + generic_commit_status.context = nil + generic_commit_status.stage = nil + generic_commit_status.save + end + + describe :context do + subject { generic_commit_status.context } + + it { is_expected.to_not be_nil } + end + + describe :stage do + subject { generic_commit_status.stage } + + it { is_expected.to_not be_nil } + end + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 17a49013d25..6aaf1c036b0 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -165,6 +165,17 @@ describe MergeRequest do end end + describe "#hook_attrs" do + it "has all the required keys" do + attrs = subject.hook_attrs + attrs = attrs.to_h + expect(attrs).to include(:source) + expect(attrs).to include(:target) + expect(attrs).to include(:last_commit) + expect(attrs).to include(:work_in_progress) + end + end + it_behaves_like 'an editable mentionable' do subject { create(:merge_request) } diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb new file mode 100644 index 00000000000..f8a3493f52d --- /dev/null +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -0,0 +1,56 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe BambooService, models: true do + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe "Execute" do + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + @bamboo_service = BambooService.create( + project: create(:project), + properties: { + bamboo_url: 'http://gitlab.com', + username: 'mic', + password: "password" + } + ) + end + + it "reset password if url changed" do + @bamboo_service.bamboo_url = 'http://gitlab1.com' + @bamboo_service.save + expect(@bamboo_service.password).to be_nil + end + + it "does not reset password if username changed" do + @bamboo_service.username = "some_name" + @bamboo_service.save + expect(@bamboo_service.password).to eq("password") + end + end +end diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb new file mode 100644 index 00000000000..3dbd2346bcc --- /dev/null +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -0,0 +1,56 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe TeamcityService, models: true do + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe "Execute" do + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + @teamcity_service = TeamcityService.create( + project: create(:project), + properties: { + teamcity_url: 'http://gitlab.com', + username: 'mic', + password: "password" + } + ) + end + + it "reset password if url changed" do + @teamcity_service.teamcity_url = 'http://gitlab1.com' + @teamcity_service.save + expect(@teamcity_service.password).to be_nil + end + + it "does not reset password if username changed" do + @teamcity_service.username = "some_name" + @teamcity_service.save + expect(@teamcity_service.password).to eq("password") + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 8b5d2c3a1c1..f93935ebe3b 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -423,4 +423,42 @@ describe Project do it { expect(project.gitlab_ci?).to be_truthy } it { expect(project.gitlab_ci_project).to be_a(Ci::Project) } end + + describe '.trending' do + let(:group) { create(:group) } + let(:project1) { create(:empty_project, :public, group: group) } + let(:project2) { create(:empty_project, :public, group: group) } + + before do + 2.times do + create(:note_on_commit, project: project1) + end + + create(:note_on_commit, project: project2) + end + + describe 'without an explicit start date' do + subject { described_class.trending.to_a } + + it 'sorts Projects by the amount of notes in descending order' do + expect(subject).to eq([project1, project2]) + end + end + + describe 'with an explicit start date' do + let(:date) { 2.months.ago } + + subject { described_class.trending(date).to_a } + + before do + 2.times do + create(:note_on_commit, project: project2, created_at: date) + end + end + + it 'sorts Projects by the amount of notes in descending order' do + expect(subject).to eq([project2, project1]) + end + end + end end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index a213ffe6c4b..da87ea5b84f 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -103,4 +103,27 @@ describe Service do end end end + + describe "#prop_updated?" do + let(:service) do + BambooService.create( + project: create(:project), + properties: { + bamboo_url: 'http://gitlab.com', + username: 'mic', + password: "password" + } + ) + end + + it "returns false" do + service.username = "key_changed" + expect(service.prop_updated?(:bamboo_url)).to be_falsy + end + + it "returns true" do + service.bamboo_url = "http://other.com" + expect(service.prop_updated?(:bamboo_url)).to be_truthy + end + end end diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb new file mode 100644 index 00000000000..b9e6dfc15a7 --- /dev/null +++ b/spec/requests/api/commit_status_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:project) { create(:project, creator_id: user.id) } + let!(:reporter) { create(:project_member, user: user, project: project, access_level: ProjectMember::REPORTER) } + let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } + let(:commit) { project.repository.commit } + let!(:ci_commit) { project.ensure_ci_commit(commit.id) } + let(:commit_status) { create(:commit_status, commit: ci_commit) } + + describe "GET /projects/:id/repository/commits/:sha/statuses" do + context "reporter user" do + let(:statuses_id) { json_response.map { |status| status['id'] } } + + before do + @status1 = create(:commit_status, commit: ci_commit, status: 'running') + @status2 = create(:commit_status, commit: ci_commit, name: 'coverage', status: 'pending') + @status3 = create(:commit_status, commit: ci_commit, name: 'coverage', ref: 'develop', status: 'running') + @status4 = create(:commit_status, commit: ci_commit, name: 'coverage', status: 'success') + @status5 = create(:commit_status, commit: ci_commit, ref: 'develop', status: 'success') + @status6 = create(:commit_status, commit: ci_commit, status: 'success') + end + + it "should return latest commit statuses" do + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user) + expect(response.status).to eq(200) + + expect(json_response).to be_an Array + expect(statuses_id).to contain_exactly(@status3.id, @status4.id, @status5.id, @status6.id) + end + + it "should return all commit statuses" do + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?all=1", user) + expect(response.status).to eq(200) + + expect(json_response).to be_an Array + expect(statuses_id).to contain_exactly(@status1.id, @status2.id, @status3.id, @status4.id, @status5.id, @status6.id) + end + + it "should return latest commit statuses for specific ref" do + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?ref=develop", user) + expect(response.status).to eq(200) + + expect(json_response).to be_an Array + expect(statuses_id).to contain_exactly(@status3.id, @status5.id) + end + + it "should return latest commit statuses for specific name" do + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?name=coverage", user) + expect(response.status).to eq(200) + + expect(json_response).to be_an Array + expect(statuses_id).to contain_exactly(@status3.id, @status4.id) + end + end + + context "guest user" do + it "should not return project commits" do + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user2) + expect(response.status).to eq(403) + end + end + + context "unauthorized user" do + it "should not return project commits" do + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses") + expect(response.status).to eq(401) + end + end + end + + describe 'POST /projects/:id/statuses/:sha' do + let(:post_url) { "/projects/#{project.id}/statuses/#{commit.id}" } + + context 'reporter user' do + context 'should create commit status' do + it 'with only required parameters' do + post api(post_url, user), state: 'success' + expect(response.status).to eq(201) + expect(json_response['sha']).to eq(commit.id) + expect(json_response['status']).to eq('success') + expect(json_response['name']).to eq('default') + expect(json_response['ref']).to be_nil + expect(json_response['target_url']).to be_nil + expect(json_response['description']).to be_nil + end + + it 'with all optional parameters' do + post api(post_url, user), state: 'success', context: 'coverage', ref: 'develop', target_url: 'url', description: 'test' + expect(response.status).to eq(201) + expect(json_response['sha']).to eq(commit.id) + expect(json_response['status']).to eq('success') + expect(json_response['name']).to eq('coverage') + expect(json_response['ref']).to eq('develop') + expect(json_response['target_url']).to eq('url') + expect(json_response['description']).to eq('test') + end + end + + context 'should not create commit status' do + it 'with invalid state' do + post api(post_url, user), state: 'invalid' + expect(response.status).to eq(400) + end + + it 'without state' do + post api(post_url, user) + expect(response.status).to eq(400) + end + + it 'invalid commit' do + post api("/projects/#{project.id}/statuses/invalid_sha", user), state: 'running' + expect(response.status).to eq(404) + end + end + end + + context 'guest user' do + it 'should not create commit status' do + post api(post_url, user2) + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not create commit status' do + post api(post_url) + expect(response.status).to eq(401) + end + end + end +end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index a1c248c636e..49acc3368f4 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -47,6 +47,19 @@ describe API::API, api: true do get api("/projects/#{project.id}/repository/commits/invalid_sha", user) expect(response.status).to eq(404) end + + it "should return not_found for CI status" do + get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + expect(response.status).to eq(200) + expect(json_response['status']).to eq('not_found') + end + + it "should return status for CI" do + ci_commit = project.ensure_ci_commit(project.repository.commit.sha) + get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + expect(response.status).to eq(200) + expect(json_response['status']).to eq(ci_commit.status) + end end context "unauthorized user" do diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index 9aa60826f21..c0226605a23 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -3,6 +3,8 @@ require "spec_helper" describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } + let(:admin) { create(:admin) } + let(:user2) { create(:user) } let(:project) {create(:project, creator_id: user.id, namespace: user.namespace) } Service.available_services_names.each do |service| @@ -51,11 +53,40 @@ describe API::API, api: true do describe "GET /projects/:id/services/#{service.dasherize}" do include_context service - it "should get #{service} settings" do + # inject some properties into the service + before do + project.build_missing_services + service_object = project.send(service_method) + service_object.properties = service_attrs + service_object.save + end + + it 'should return authentication error when unauthenticated' do + get api("/projects/#{project.id}/services/#{dashed_service}") + expect(response.status).to eq(401) + end + + it "should return all properties of service #{service} when authenticated as admin" do + get api("/projects/#{project.id}/services/#{dashed_service}", admin) + + expect(response.status).to eq(200) + expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list.map) + end + + it "should return properties of service #{service} other than passwords when authenticated as project owner" do get api("/projects/#{project.id}/services/#{dashed_service}", user) expect(response.status).to eq(200) + expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list_without_passwords) end + + it "should return error when authenticated but not a project owner" do + project.team << [user2, :developer] + get api("/projects/#{project.id}/services/#{dashed_service}", user2) + + expect(response.status).to eq(403) + end + end end end diff --git a/spec/requests/ci/builds_spec.rb b/spec/requests/ci/builds_spec.rb deleted file mode 100644 index f68116c52aa..00000000000 --- a/spec/requests/ci/builds_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'spec_helper' - -describe "Builds" do - before do - @commit = FactoryGirl.create :ci_commit - @build = FactoryGirl.create :ci_build, commit: @commit - end - - describe "GET /:project/builds/:id/status.json" do - before do - get status_ci_project_build_path(@commit.project, @build), format: :json - end - - it { expect(response.status).to eq(200) } - it { expect(response.body).to include(@build.sha) } - end -end diff --git a/spec/requests/ci/commits_spec.rb b/spec/requests/ci/commits_spec.rb deleted file mode 100644 index f43a3982d71..00000000000 --- a/spec/requests/ci/commits_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -describe "Commits" do - before do - @commit = FactoryGirl.create :ci_commit - end - - describe "GET /:project/refs/:ref_name/commits/:id/status.json" do - before do - get status_ci_project_commits_path(@commit.project, @commit.sha), format: :json - end - - it { expect(response.status).to eq(200) } - it { expect(response.body).to include(@commit.sha) } - end -end diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb index f12e09c58c3..ddee2e62dfc 100644 --- a/spec/services/projects/download_service_spec.rb +++ b/spec/services/projects/download_service_spec.rb @@ -37,7 +37,6 @@ describe Projects::DownloadService do it { expect(@link_to_file).to have_key('url') } it { expect(@link_to_file).to have_key('is_image') } it { expect(@link_to_file['is_image']).to be true } - it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") } it { expect(@link_to_file['url']).to match('rails_sample.jpg') } it { expect(@link_to_file['alt']).to eq('rails_sample') } end @@ -52,7 +51,6 @@ describe Projects::DownloadService do it { expect(@link_to_file).to have_key('url') } it { expect(@link_to_file).to have_key('is_image') } it { expect(@link_to_file['is_image']).to be false } - it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") } it { expect(@link_to_file['url']).to match('doc_sample.txt') } it { expect(@link_to_file['alt']).to eq('doc_sample.txt') } end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index bb7da33b12e..47755bfc990 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -7,6 +7,8 @@ describe Projects::TransferService do context 'namespace -> namespace' do before do + allow_any_instance_of(Gitlab::UploadsTransfer). + to receive(:move_project).and_return(true) group.add_owner(user) @result = transfer_project(project, user, group) end diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb index fa4ff6b01ad..1b1a80d1fe7 100644 --- a/spec/services/projects/upload_service_spec.rb +++ b/spec/services/projects/upload_service_spec.rb @@ -18,7 +18,6 @@ describe Projects::UploadService do it { expect(@link_to_file).to have_key(:is_image) } it { expect(@link_to_file).to have_value('banana_sample') } it { expect(@link_to_file[:is_image]).to equal(true) } - it { expect(@link_to_file[:url]).to match("/#{@project.path_with_namespace}") } it { expect(@link_to_file[:url]).to match('banana_sample.gif') } end @@ -34,7 +33,6 @@ describe Projects::UploadService do it { expect(@link_to_file).to have_value('dk') } it { expect(@link_to_file).to have_key(:is_image) } it { expect(@link_to_file[:is_image]).to equal(true) } - it { expect(@link_to_file[:url]).to match("/#{@project.path_with_namespace}") } it { expect(@link_to_file[:url]).to match('dk.png') } end @@ -49,7 +47,6 @@ describe Projects::UploadService do it { expect(@link_to_file).to have_key(:is_image) } it { expect(@link_to_file).to have_value('rails_sample') } it { expect(@link_to_file[:is_image]).to equal(true) } - it { expect(@link_to_file[:url]).to match("/#{@project.path_with_namespace}") } it { expect(@link_to_file[:url]).to match('rails_sample.jpg') } end @@ -64,7 +61,6 @@ describe Projects::UploadService do it { expect(@link_to_file).to have_key(:is_image) } it { expect(@link_to_file).to have_value('doc_sample.txt') } it { expect(@link_to_file[:is_image]).to equal(false) } - it { expect(@link_to_file[:url]).to match("/#{@project.path_with_namespace}") } it { expect(@link_to_file[:url]).to match('doc_sample.txt') } end diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index 48c49e2f717..a31fc1e4b07 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -13,8 +13,8 @@ describe SystemHooksService do it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :email, :user_id) } it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } - it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :project_name, :project_path, :project_id, :user_name, :user_email, :access_level, :project_visibility) } - it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :project_name, :project_path, :project_id, :user_name, :user_email, :access_level, :project_visibility) } + it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) } + it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) } it { expect(event_data(key, :create)).to include(:username, :key, :id) } it { expect(event_data(key, :destroy)).to include(:username, :key, :id) } diff --git a/spec/support/services_shared_context.rb b/spec/support/services_shared_context.rb index 4d007ae55ee..d1c999cad4d 100644 --- a/spec/support/services_shared_context.rb +++ b/spec/support/services_shared_context.rb @@ -3,7 +3,13 @@ Service.available_services_names.each do |service| let(:dashed_service) { service.dasherize } let(:service_method) { "#{service}_service".to_sym } let(:service_klass) { "#{service}_service".classify.constantize } - let(:service_attrs_list) { service_klass.new.fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } } + let(:service_fields) { service_klass.new.fields } + let(:service_attrs_list) { service_fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } } + let(:service_attrs_list_without_passwords) do + service_fields. + select { |field| field[:type] != 'password' }. + map { |field| field[:name].to_sym} + end let(:service_attrs) do service_attrs_list.inject({}) do |hash, k| if k =~ /^(token*|.*_token|.*_key)/