diff options
166 files changed, 3162 insertions, 1056 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index cd13f581517..3aac8401848 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -284,7 +284,7 @@ Style/IfWithSemicolon: # Checks that conditional statements do not have an identical line at the # end of each branch, which can validly be moved out of the conditional. Style/IdenticalConditionalBranches: - Enabled: false + Enabled: true # Checks the indentation of the first line of the right-hand-side of a # multi-line assignment. diff --git a/CHANGELOG b/CHANGELOG index 2c58a312208..eccdcc988fa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ v 8.10.0 (unreleased) - Replace Haml with Hamlit to make view rendering faster. !3666 - Refactor repository paths handling to allow multiple git mount points - Add Application Setting to configure default Repository Path for new projects + - Remove pinTo from Flash and make inline flash messages look nicer !4854 (winniehell) - Wrap code blocks on Activies and Todos page. !4783 (winniehell) - Align flash messages with left side of page content !4959 (winniehell) - Display last commit of deleted branch in push events !4699 (winniehell) @@ -18,27 +19,35 @@ v 8.10.0 (unreleased) - Fix MR-auto-close text added to description. !4836 - Fix issue, preventing users w/o push access to sort tags !5105 (redetection) - Add Spring EmojiOne updates. + - Add syntax for multiline blockquote using `>>>` fence !3954 - Fix viewing notification settings when a project is pending deletion - Fix pagination when sorting by columns with lots of ties (like priority) + - The Markdown reference parsers now re-use query results to prevent running the same queries multiple times !5020 - Updated project header design - Exclude email check from the standard health check - Updated layout for Projects, Groups, Users on Admin area !4424 - Fix changing issue state columns in milestone view - Add notification settings dropdown for groups + - Wildcards for protected branches. !4665 - Allow importing from Github using Personal Access Tokens. (Eric K Idema) - API: Todos !3188 (Robert Schilling) + - API: Expose shared groups for projects and shared projects for groups !5050 (Robert Schilling) - Add "Enabled Git access protocols" to Application Settings - Diffs will create button/diff form on demand no on server side - Reduce size of HTML used by diff comment forms - Fix user creation with stronger minimum password requirements !4054 (nathan-pmt) + - Only show New Snippet button to users that can create snippets. - PipelinesFinder uses git cache data - Throttle the update of `project.pushes_since_gc` to 1 minute. + - Allow expanding and collapsing files in diff view (!4990) + - Collapse large diffs by default (!4990) - Check for conflicts with existing Project's wiki path when creating a new project. - Show last push widget in upstream after push to fork - Don't instantiate a git tree on Projects show default view - Bump Rinku to 2.0.0 - Remove unused front-end variable -> default_issues_tracker - Better caching of git calls on ProjectsController#show. + - Avoid to retrieve MR closes_issues as much as possible. - Add API endpoint for a group issues !4520 (mahcsig) - Add Bugzilla integration !4930 (iamtjg) - Instrument Rinku usage @@ -46,6 +55,8 @@ v 8.10.0 (unreleased) - RailsCache metris now includes fetch_hit/fetch_miss and read_hit/read_miss info. - Allow [ci skip] to be in any case and allow [skip ci]. !4785 (simon_w) - Set import_url validation to be more strict + - Memoize MR merged/closed events retrieval + - Don't render discussion notes when requesting diff tab through AJAX - Add basic system information like memory and disk usage to the admin panel - Don't garbage collect commits that have related DB records like comments - More descriptive message for git hooks and file locks @@ -54,6 +65,14 @@ v 8.10.0 (unreleased) - Fix importer for GitHub Pull Requests when a branch was reused across Pull Requests - Add date when user joined the team on the member page - Fix 404 redirect after validation fails importing a GitLab project + - Fix 404 redirect after validation fails importing a GitLab project + - Added setting to set new users by default as external !4545 (Dravere) + - Add min value for project limit field on user's form !3622 (jastkand) + - Add reminder to not paste private SSH keys !4399 (Ingo Blechschmidt) + - Remove duplicate `description` field in `MergeRequest` entities (Ben Boeckel) + +v 8.9.6 (unreleased) + - Fix importing of events under notes for GitLab projects v 8.9.5 - Add more debug info to import/export and memory killer. !5108 @@ -98,7 +117,7 @@ v 8.9.3 - Removed fade when filtering results. !4932 - Fix missing avatar on system notes. !4954 - Reduce overhead and optimize ProjectTeam#max_member_access performance. !4973 - - Use update_columns to by_pass all the dirty code on active_record. !4985 + - Use update_columns to bypass all the dirty code on active_record. !4985 - Fix restore Rake task warning message output !4980 v 8.9.2 @@ -2602,13 +2621,13 @@ v 6.5.0 - Files API supports base64 encoded content (sponsored by O'Reilly Media) - Added support for Go's repository retrieval (Bruno Albuquerque) -v6.4.3 +v 6.4.3 - Don't use unicorn worker killer if PhusionPassenger is defined -v6.4.2 +v 6.4.2 - Fixed wrong behaviour of script/upgrade.rb -v6.4.1 +v 6.4.1 - Fixed bug with repository rename - Fixed bug with project transfer @@ -1,4 +1,4 @@ -source "https://rubygems.org" +source 'https://rubygems.org' gem 'rails', '4.2.6' gem 'rails-deprecated_sanitizer', '~> 1.0.3' @@ -11,11 +11,11 @@ gem 'responders', '~> 2.0' gem 'sprockets', '~> 3.6.0' # Default values for AR models -gem "default_value_for", "~> 3.0.0" +gem 'default_value_for', '~> 3.0.0' # Supported DBs -gem "mysql2", '~> 0.3.16', group: :mysql -gem "pg", '~> 0.18.2', group: :postgres +gem 'mysql2', '~> 0.3.16', group: :mysql +gem 'pg', '~> 0.18.2', group: :postgres # Authentication libraries gem 'devise', '~> 4.0' @@ -28,7 +28,7 @@ gem 'omniauth-cas3', '~> 1.1.2' gem 'omniauth-facebook', '~> 3.0.0' gem 'omniauth-github', '~> 1.1.1' gem 'omniauth-gitlab', '~> 1.0.0' -gem 'omniauth-google-oauth2', '~> 0.2.0' +gem 'omniauth-google-oauth2', '~> 0.4.1' gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-saml', '~> 1.6.0' gem 'omniauth-shibboleth', '~> 1.2.0' @@ -48,16 +48,16 @@ gem 'attr_encrypted', '~> 3.0.0' gem 'u2f', '~> 0.2.1' # Browser detection -gem "browser", '~> 2.2' +gem 'browser', '~> 2.2' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 10.2' +gem 'gitlab_git', '~> 10.2' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes # see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master -gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: "omniauth-ldap" +gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: 'omniauth-ldap' # Git Wiki # Required manually in config/initializers/gollum.rb to control load order @@ -65,7 +65,7 @@ gem 'gollum-lib', '~> 4.1.0', require: false gem 'gollum-rugged_adapter', '~> 0.4.2', require: false # Language detection -gem "github-linguist", "~> 4.7.0", require: "linguist" +gem 'github-linguist', '~> 4.7.0', require: 'linguist' # API gem 'grape', '~> 0.13.0' @@ -73,13 +73,13 @@ gem 'grape-entity', '~> 0.4.2' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' # Pagination -gem "kaminari", "~> 0.17.0" +gem 'kaminari', '~> 0.17.0' # HAML gem 'hamlit', '~> 2.5' # Files attachments -gem "carrierwave", '~> 0.10.0' +gem 'carrierwave', '~> 0.10.0' # Drag and Drop UI gem 'dropzonejs-rails', '~> 0.7.1' @@ -94,13 +94,13 @@ gem 'fog-openstack', '~> 0.1' gem 'fog-rackspace', '~> 0.1.1' # for aws storage -gem "unf", '~> 0.1.4' +gem 'unf', '~> 0.1.4' # Authorization -gem "six", '~> 0.2.0' +gem 'six', '~> 0.2.0' # Seed data -gem "seed-fu", '~> 2.3.5' +gem 'seed-fu', '~> 2.3.5' # Markdown and HTML processing gem 'html-pipeline', '~> 1.11.0' @@ -124,29 +124,29 @@ gem 'diffy', '~> 3.0.3' # Application server group :unicorn do - gem "unicorn", '~> 4.9.0' + gem 'unicorn', '~> 4.9.0' gem 'unicorn-worker-killer', '~> 0.4.2' end # State machine -gem "state_machines-activerecord", '~> 0.4.0' +gem 'state_machines-activerecord', '~> 0.4.0' # Run events after state machine commits -gem 'after_commit_queue' +gem 'after_commit_queue', '~> 1.3.0' # Issue tags gem 'acts-as-taggable-on', '~> 3.4' # Background jobs -gem 'sinatra', '~> 1.4.4', require: nil +gem 'sinatra', '~> 1.4.4', require: false gem 'sidekiq', '~> 4.0' gem 'sidekiq-cron', '~> 0.4.0' -gem 'redis-namespace' +gem 'redis-namespace', '~> 1.5.2' # HTTP requests -gem "httparty", '~> 0.13.3' +gem 'httparty', '~> 0.13.3' # Colored output to console -gem "rainbow", '~> 2.1.0' +gem 'rainbow', '~> 2.1.0' # GitLab settings gem 'settingslogic', '~> 2.0.9' @@ -156,7 +156,7 @@ gem 'settingslogic', '~> 2.0.9' gem 'version_sorter', '~> 2.0.0' # Cache -gem "redis-rails", '~> 4.0.0' +gem 'redis-rails', '~> 4.0.0' # Redis gem 'redis', '~> 3.2' @@ -169,13 +169,13 @@ gem 'tinder', '~> 1.10.0' gem 'hipchat', '~> 1.5.0' # Flowdock integration -gem "gitlab-flowdock-git-hook", "~> 1.0.1" +gem 'gitlab-flowdock-git-hook', '~> 1.0.1' # Gemnasium integration -gem "gemnasium-gitlab-service", "~> 0.2" +gem 'gemnasium-gitlab-service', '~> 0.2' # Slack integration -gem "slack-notifier", "~> 1.2.0" +gem 'slack-notifier', '~> 1.2.0' # Asana integration gem 'asana', '~> 0.4.0' @@ -187,20 +187,20 @@ gem 'ruby-fogbugz', '~> 0.2.1' gem 'd3_rails', '~> 3.5.0' # underscore-rails -gem "underscore-rails", "~> 1.8.0" +gem 'underscore-rails', '~> 1.8.0' # Sanitize user input -gem "sanitize", '~> 2.0' +gem 'sanitize', '~> 2.0' gem 'babosa', '~> 1.0.2' # Sanitizes SVG input -gem "loofah", "~> 2.0.3" +gem 'loofah', '~> 2.0.3' # Working with license gem 'licensee', '~> 8.0.0' # Protect against bruteforcing -gem "rack-attack", '~> 4.3.1' +gem 'rack-attack', '~> 4.3.1' # Ace editor gem 'ace-rails-ap', '~> 4.0.2' @@ -214,9 +214,9 @@ gem 'charlock_holmes', '~> 0.7.3' # Parse duration gem 'chronic_duration', '~> 0.10.6' -gem "sass-rails", '~> 5.0.0' -gem "coffee-rails", '~> 4.1.0' -gem "uglifier", '~> 2.7.2' +gem 'sass-rails', '~> 5.0.0' +gem 'coffee-rails', '~> 4.1.0' +gem 'uglifier', '~> 2.7.2' gem 'turbolinks', '~> 2.5.0' gem 'jquery-turbolinks', '~> 2.1.0' @@ -247,13 +247,13 @@ group :metrics do end group :development do - gem "foreman" + gem 'foreman', '~> 0.78.0' gem 'brakeman', '~> 3.3.0', require: false gem 'letter_opener_web', '~> 1.3.0' gem 'rerun', '~> 0.11.0' - gem 'bullet', require: false - gem 'rblineprof', platform: :mri, require: false + gem 'bullet', '~> 5.0.0', require: false + gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false gem 'web-console', '~> 2.0' # Better errors handler @@ -261,15 +261,15 @@ group :development do gem 'binding_of_caller', '~> 0.7.2' # Docs generator - gem "sdoc", '~> 0.3.20' + gem 'sdoc', '~> 0.3.20' # thin instead webrick gem 'thin', '~> 1.7.0' end group :development, :test do - gem 'byebug', platform: :mri - gem 'pry-rails' + gem 'byebug', '~> 8.2.1', platform: :mri + gem 'pry-rails', '~> 0.3.4' gem 'awesome_print', '~> 1.2.0', require: false gem 'fuubar', '~> 2.0.0' @@ -277,7 +277,7 @@ group :development, :test do gem 'database_cleaner', '~> 1.4.0' gem 'factory_girl_rails', '~> 4.6.0' gem 'rspec-rails', '~> 3.5.0' - gem 'rspec-retry' + gem 'rspec-retry', '~> 0.4.5' gem 'spinach-rails', '~> 0.2.1' gem 'spinach-rerun-reporter', '~> 0.0.2' @@ -303,14 +303,14 @@ group :development, :test do gem 'rubocop-rspec', '~> 1.5.0', require: false gem 'scss_lint', '~> 0.47.0', require: false gem 'simplecov', '~> 0.11.0', require: false - gem 'flog', require: false - gem 'flay', require: false - gem 'bundler-audit', require: false + gem 'flog', '~> 4.3.2', require: false + gem 'flay', '~> 2.6.1', require: false + gem 'bundler-audit', '~> 0.5.0', require: false - gem 'benchmark-ips', require: false + gem 'benchmark-ips', '~> 2.3.0', require: false - gem "license_finder", require: false - gem 'knapsack' + gem 'license_finder', '~> 2.1.0', require: false + gem 'knapsack', '~> 1.11.0' end group :test do @@ -318,30 +318,30 @@ group :test do gem 'email_spec', '~> 1.6.0' gem 'webmock', '~> 1.21.0' gem 'test_after_commit', '~> 0.4.2' - gem 'sham_rack' + gem 'sham_rack', '~> 1.3.6' end group :production do - gem "gitlab_meta", '7.0' + gem 'gitlab_meta', '7.0' end -gem "newrelic_rpm", '~> 3.14' +gem 'newrelic_rpm', '~> 3.14' gem 'octokit', '~> 4.3.0' -gem "mail_room", "~> 0.8" +gem 'mail_room', '~> 0.8' gem 'email_reply_parser', '~> 0.5.8' ## CI gem 'activerecord-session_store', '~> 1.0.0' -gem "nested_form", '~> 0.3.2' +gem 'nested_form', '~> 0.3.2' # OAuth -gem 'oauth2', '~> 1.0.0' +gem 'oauth2', '~> 1.2.0' # Soft deletion -gem "paranoia", "~> 2.0" +gem 'paranoia', '~> 2.0' # Health check gem 'health_check', '~> 1.5.1' diff --git a/Gemfile.lock b/Gemfile.lock index af8fbedc127..721ab9ddc5d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -353,7 +353,7 @@ GEM jquery-ui-rails (5.0.5) railties (>= 3.2.16) json (1.8.3) - jwt (1.5.2) + jwt (1.5.4) kaminari (0.17.0) actionpack (>= 3.0.0) activesupport (>= 3.0.0) @@ -393,7 +393,7 @@ GEM mini_portile2 (2.1.0) minitest (5.7.0) mousetrap-rails (1.4.6) - multi_json (1.11.2) + multi_json (1.12.1) multi_xml (0.5.5) multipart-post (2.0.0) mysql2 (0.3.20) @@ -406,12 +406,12 @@ GEM pkg-config (~> 1.1.7) numerizer (0.1.1) oauth (0.4.7) - oauth2 (1.0.0) + oauth2 (1.2.0) faraday (>= 0.8, < 0.10) jwt (~> 1.0) multi_json (~> 1.3) multi_xml (~> 0.5) - rack (~> 1.2) + rack (>= 1.2, < 3) octokit (4.3.0) sawyer (~> 0.7.0, >= 0.5.3) omniauth (1.3.1) @@ -439,7 +439,7 @@ GEM omniauth-gitlab (1.0.1) omniauth (~> 1.0) omniauth-oauth2 (~> 1.0) - omniauth-google-oauth2 (0.2.10) + omniauth-google-oauth2 (0.4.1) addressable (~> 2.3) jwt (~> 1.0) multi_json (~> 1.3) @@ -806,7 +806,7 @@ DEPENDENCIES activerecord-session_store (~> 1.0.0) acts-as-taggable-on (~> 3.4) addressable (~> 2.3.8) - after_commit_queue + after_commit_queue (~> 1.3.0) akismet (~> 2.0) allocations (~> 1.0) asana (~> 0.4.0) @@ -815,15 +815,15 @@ DEPENDENCIES awesome_print (~> 1.2.0) babosa (~> 1.0.2) base32 (~> 0.3.0) - benchmark-ips + benchmark-ips (~> 2.3.0) better_errors (~> 1.0.1) binding_of_caller (~> 0.7.2) bootstrap-sass (~> 3.3.0) brakeman (~> 3.3.0) browser (~> 2.2) - bullet - bundler-audit - byebug + bullet (~> 5.0.0) + bundler-audit (~> 0.5.0) + byebug (~> 8.2.1) capybara (~> 2.6.2) capybara-screenshot (~> 1.0.0) carrierwave (~> 0.10.0) @@ -844,8 +844,8 @@ DEPENDENCIES email_spec (~> 1.6.0) factory_girl_rails (~> 4.6.0) ffaker (~> 2.0.0) - flay - flog + flay (~> 2.6.1) + flog (~> 4.3.2) fog-aws (~> 0.9) fog-azure (~> 0.0) fog-core (~> 1.40) @@ -854,7 +854,7 @@ DEPENDENCIES fog-openstack (~> 0.1) fog-rackspace (~> 0.1.1) font-awesome-rails (~> 4.6.1) - foreman + foreman (~> 0.78.0) fuubar (~> 2.0.0) gemnasium-gitlab-service (~> 0.2) gemojione (~> 2.6) @@ -881,9 +881,9 @@ DEPENDENCIES jquery-ui-rails (~> 5.0.0) jwt kaminari (~> 0.17.0) - knapsack + knapsack (~> 1.11.0) letter_opener_web (~> 1.3.0) - license_finder + license_finder (~> 2.1.0) licensee (~> 8.0.0) loofah (~> 2.0.3) mail_room (~> 0.8) @@ -895,7 +895,7 @@ DEPENDENCIES net-ssh (~> 3.0.1) newrelic_rpm (~> 3.14) nokogiri (~> 1.6.7, >= 1.6.7.2) - oauth2 (~> 1.0.0) + oauth2 (~> 1.2.0) octokit (~> 4.3.0) omniauth (~> 1.3.1) omniauth-auth0 (~> 1.4.1) @@ -905,7 +905,7 @@ DEPENDENCIES omniauth-facebook (~> 3.0.0) omniauth-github (~> 1.1.1) omniauth-gitlab (~> 1.0.0) - omniauth-google-oauth2 (~> 0.2.0) + omniauth-google-oauth2 (~> 0.4.1) omniauth-kerberos (~> 0.3.0) omniauth-saml (~> 1.6.0) omniauth-shibboleth (~> 1.2.0) @@ -916,19 +916,19 @@ DEPENDENCIES pg (~> 0.18.2) poltergeist (~> 1.9.0) premailer-rails (~> 1.9.0) - pry-rails + pry-rails (~> 0.3.4) rack-attack (~> 4.3.1) rack-cors (~> 0.4.0) rack-oauth2 (~> 1.2.1) rails (= 4.2.6) rails-deprecated_sanitizer (~> 1.0.3) rainbow (~> 2.1.0) - rblineprof + rblineprof (~> 0.3.6) rdoc (~> 3.6) recaptcha (~> 3.0) redcarpet (~> 3.3.3) redis (~> 3.2) - redis-namespace + redis-namespace (~> 1.5.2) redis-rails (~> 4.0.0) request_store (~> 1.3.0) rerun (~> 0.11.0) @@ -936,7 +936,7 @@ DEPENDENCIES rouge (~> 1.11) rqrcode-rails3 (~> 0.1.7) rspec-rails (~> 3.5.0) - rspec-retry + rspec-retry (~> 0.4.5) rubocop (~> 0.40.0) rubocop-rspec (~> 1.5.0) ruby-fogbugz (~> 0.2.1) @@ -948,7 +948,7 @@ DEPENDENCIES select2-rails (~> 3.5.9) sentry-raven (~> 1.1.0) settingslogic (~> 2.0.9) - sham_rack + sham_rack (~> 1.3.6) shoulda-matchers (~> 2.8.0) sidekiq (~> 4.0) sidekiq-cron (~> 0.4.0) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 20fe5a5cc27..c98763d6271 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -47,14 +47,12 @@ #= require date.format #= require_directory ./behaviors #= require_directory ./blob -#= require_directory ./ci #= require_directory ./commit #= require_directory ./extensions #= require_directory ./lib/utils #= require_directory ./u2f #= require_directory . #= require fuzzaldrin-plus -#= require cropper #= require u2f window.slugify = (text) -> diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/build.coffee index 74691b2c1b5..cf203ea43a0 100644 --- a/app/assets/javascripts/ci/build.coffee +++ b/app/assets/javascripts/build.coffee @@ -1,9 +1,9 @@ -class @CiBuild +class @Build @interval: null @state: null constructor: (@page_url, @build_url, @build_status, @state) -> - clearInterval(CiBuild.interval) + clearInterval(Build.interval) # Init breakpoint checker @bp = Breakpoints.get() @@ -40,7 +40,7 @@ class @CiBuild # Check for new build output if user still watching build page # Only valid for runnig build when output changes during time # - CiBuild.interval = setInterval => + Build.interval = setInterval => if window.location.href.split("#").first() is @page_url @getBuildTrace() , 4000 diff --git a/app/assets/javascripts/ci/application.js.coffee b/app/assets/javascripts/ci/application.js.coffee deleted file mode 100644 index ca24c1d759f..00000000000 --- a/app/assets/javascripts/ci/application.js.coffee +++ /dev/null @@ -1,12 +0,0 @@ -#= require pager -#= require jquery_nested_form -#= require_tree . - -$(document).on 'click', '.assign-all-runner', -> - $(this).replaceWith('<i class="fa fa-refresh fa-spin"></i> Assign in progress..') - -window.unbindEvents = -> - $(document).unbind('scroll') - $(document).off('scroll') - -document.addEventListener("page:fetch", unbindEvents) diff --git a/app/assets/javascripts/ci/projects.js.coffee b/app/assets/javascripts/ci/projects.js.coffee deleted file mode 100644 index e6406011d11..00000000000 --- a/app/assets/javascripts/ci/projects.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -$(document).on 'click', '.badge-codes-toggle', -> - $('.badge-codes-block').toggleClass("hide") - return false diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index a39df421832..74fd77cf7ab 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -131,7 +131,6 @@ class Dispatcher when 'dashboard', 'root' shortcut_handler = new ShortcutsDashboardNavigation() when 'profiles' - new Profile() new NotificationsForm() new NotificationsDropdown() when 'projects' diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee index b76d214790a..5a493041538 100644 --- a/app/assets/javascripts/flash.js.coffee +++ b/app/assets/javascripts/flash.js.coffee @@ -1,24 +1,28 @@ class @Flash - constructor: (message, type = 'alert')-> - @flash = $(".flash-container") - @flash.html("") + hideFlash = -> $(@).fadeOut() - innerDiv = $('<div/>', + constructor: (message, type = 'alert', parent = null)-> + if parent + @flashContainer = parent.find('.flash-container') + else + @flashContainer = $('.flash-container-page') + + @flashContainer.html('') + + flash = $('<div/>', class: "flash-#{type}" ) - innerDiv.appendTo(".flash-container") + flash.on 'click', hideFlash - textDiv = $("<div/>", - class: "flash-text", + textDiv = $('<div/>', + class: 'flash-text', text: message ) - textDiv.appendTo(innerDiv) + textDiv.appendTo(flash) - if @flash.parent().hasClass('content-wrapper') + if @flashContainer.parent().hasClass('content-wrapper') textDiv.addClass('container-fluid container-limited') - @flash.click -> $(@).fadeOut() - @flash.show() + flash.appendTo(@flashContainer) + @flashContainer.show() - pinTo: (selector) -> - @flash.detach().appendTo(selector) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index ed9dfcc917e..1c65e833d47 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -56,6 +56,7 @@ class GitLabDropdownFilter return BLUR_KEYCODES.indexOf(keyCode) >= 0 filter: (search_text) -> + @options.onFilter(search_text) if @options.onFilter data = @options.data() if data? and not @options.filterByText @@ -195,6 +196,7 @@ class GitLabDropdown @filter = new GitLabDropdownFilter @filterInput, filterInputBlur: @filterInputBlur filterByText: @options.filterByText + onFilter: @options.onFilter remote: @options.filterRemote query: @options.data keys: searchFields @@ -530,7 +532,7 @@ class GitLabDropdown if $el.length e.preventDefault() e.stopImmediatePropagation() - $(selector, @dropdown)[0].click() + $el.first().trigger('click') addArrowKeyEvent: -> ARROW_KEY_CODES = [38, 40] diff --git a/app/assets/javascripts/lib/cropper.js.coffee b/app/assets/javascripts/lib/cropper.js.coffee new file mode 100644 index 00000000000..32536d23fe3 --- /dev/null +++ b/app/assets/javascripts/lib/cropper.js.coffee @@ -0,0 +1 @@ +#= require cropper diff --git a/app/assets/javascripts/lib/utils/common_utils.js.coffee b/app/assets/javascripts/lib/utils/common_utils.js.coffee index e39dcb2daa9..d4dd3dc329a 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.coffee +++ b/app/assets/javascripts/lib/utils/common_utils.js.coffee @@ -5,12 +5,12 @@ w.gl.utils.isInGroupsPage = -> - return $('body').data('page').split(':')[0] is 'groups' + return gl.utils.getPagePath() is 'groups' w.gl.utils.isInProjectPage = -> - return $('body').data('page').split(':')[0] is 'projects' + return gl.utils.getPagePath() is 'projects' w.gl.utils.getProjectSlug = -> @@ -40,6 +40,9 @@ e.stopImmediatePropagation() return false + gl.utils.getPagePath = -> + return $('body').data('page').split(':')[0] + jQuery.timefor = (time, suffix, expiredLabel) -> diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 87916823f81..86539e0d725 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -159,6 +159,7 @@ class @MergeRequestTabs $('#diffs').html data.html gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')) $('#diffs .js-syntax-highlight').syntaxHighlight() + $('#diffs .diff-file').singleFileDiff() @expandViewContainer() if @diffViewType() is 'parallel' @diffsLoaded = true @scrollToElement("#diffs") diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 0b7d8f64456..0ea54faae1a 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -194,8 +194,7 @@ class @Notes renderNote: (note) -> unless note.valid if note.award - flash = new Flash('You have already awarded this emoji!', 'alert') - flash.pinTo('.header-content') + new Flash('You have already awarded this emoji!', 'alert') return if note.award @@ -325,6 +324,8 @@ class @Notes form.find("#note_position").remove() form.find("#note_type").remove() + @parentTimeline = form.parents('.timeline') + ### General note form setup. @@ -357,8 +358,7 @@ class @Notes @renderNote(note) addNoteError: (xhr, note, status) => - flash = new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert') - flash.pinTo('.md-area') + new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', @parentTimeline) ### Called in response to the new note form being submitted diff --git a/app/assets/javascripts/profile/application.js.coffee b/app/assets/javascripts/profile/application.js.coffee new file mode 100644 index 00000000000..91cacfece46 --- /dev/null +++ b/app/assets/javascripts/profile/application.js.coffee @@ -0,0 +1,2 @@ +# +#= require_tree . diff --git a/app/assets/javascripts/gl_crop.js.coffee b/app/assets/javascripts/profile/gl_crop.js.coffee index df9bfdfa6cc..df9bfdfa6cc 100644 --- a/app/assets/javascripts/gl_crop.js.coffee +++ b/app/assets/javascripts/profile/gl_crop.js.coffee diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile/profile.js.coffee index 1583d1ba6f9..f3b05f2c646 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile/profile.js.coffee @@ -78,3 +78,6 @@ $ -> if comment && comment.length > 1 && $title.val() == '' $title.val(comment[1]).change() + + if gl.utils.getPagePath() == 'profiles' + new Profile() diff --git a/app/assets/javascripts/protected_branch_select.js.coffee b/app/assets/javascripts/protected_branch_select.js.coffee new file mode 100644 index 00000000000..6d45770ace9 --- /dev/null +++ b/app/assets/javascripts/protected_branch_select.js.coffee @@ -0,0 +1,40 @@ +class @ProtectedBranchSelect + constructor: (currentProject) -> + $('.dropdown-footer').hide(); + @dropdown = $('.js-protected-branch-select').glDropdown( + data: @getProtectedBranches + filterable: true + remote: false + search: + fields: ['title'] + selectable: true + toggleLabel: (selected) -> if (selected and 'id' of selected) then selected.title else 'Protected Branch' + fieldName: 'protected_branch[name]' + text: (protected_branch) -> _.escape(protected_branch.title) + id: (protected_branch) -> _.escape(protected_branch.id) + onFilter: @toggleCreateNewButton + clicked: () -> $('.protect-branch-btn').attr('disabled', false) + ) + + $('.create-new-protected-branch').on 'click', (event) => + # Refresh the dropdown's data, which ends up calling `getProtectedBranches` + @dropdown.data('glDropdown').remote.execute() + @dropdown.data('glDropdown').selectRowAtIndex(event, 0) + + getProtectedBranches: (term, callback) => + if @selectedBranch + callback(gon.open_branches.concat(@selectedBranch)) + else + callback(gon.open_branches) + + toggleCreateNewButton: (branchName) => + @selectedBranch = { title: branchName, id: branchName, text: branchName } + + if branchName is '' + $('.protected-branch-select-footer-list').addClass('hidden') + $('.dropdown-footer').hide(); + else + $('.create-new-protected-branch').text("Create Protected Branch: #{branchName}") + $('.protected-branch-select-footer-list').removeClass('hidden') + $('.dropdown-footer').show(); + diff --git a/app/assets/javascripts/protected_branches.js.coffee b/app/assets/javascripts/protected_branches.js.coffee index 5753c9d4e72..79c2306e4d2 100644 --- a/app/assets/javascripts/protected_branches.js.coffee +++ b/app/assets/javascripts/protected_branches.js.coffee @@ -11,7 +11,8 @@ $ -> dataType: "json" data: id: id - developers_can_push: checked + protected_branch: + developers_can_push: checked success: -> row = $(e.target) diff --git a/app/assets/javascripts/single_file_diff.js.coffee b/app/assets/javascripts/single_file_diff.js.coffee new file mode 100644 index 00000000000..f3e225c3728 --- /dev/null +++ b/app/assets/javascripts/single_file_diff.js.coffee @@ -0,0 +1,54 @@ +class @SingleFileDiff + + WRAPPER = '<div class="diff-content diff-wrap-lines"></div>' + LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>' + ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>' + COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. Click to expand it.</div>' + + constructor: (@file) -> + @content = $('.diff-content', @file) + @diffForPath = @content.find('[data-diff-for-path]').data 'diff-for-path' + @isOpen = !@diffForPath + + if @diffForPath + @collapsedContent = @content + @loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide() + @content = null + @collapsedContent.after(@loadingContent) + else + @collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide() + @content.after(@collapsedContent) + + @collapsedContent.on 'click', @toggleDiff + + $('.file-title > a', @file).on 'click', @toggleDiff + + toggleDiff: (e) => + @isOpen = !@isOpen + if not @isOpen and not @hasError + @content.hide() + @collapsedContent.show() + else if @content + @collapsedContent.hide() + @content.show() + else + @getContentHTML() + + getContentHTML: -> + @collapsedContent.hide() + @loadingContent.show() + $.get @diffForPath, (data) => + @loadingContent.hide() + if data.html + @content = $(data.html) + @content.syntaxHighlight() + else + @hasError = true + @content = $(ERROR_HTML) + @collapsedContent.after(@content) + return + +$.fn.singleFileDiff = -> + return @each -> + if not $.data this, 'singleFileDiff' + $.data this, 'singleFileDiff', new SingleFileDiff this diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss index 40b5171a8c6..d28cda6d62d 100644 --- a/app/assets/stylesheets/framework/blank.scss +++ b/app/assets/stylesheets/framework/blank.scss @@ -1,3 +1,12 @@ +.blank-state-welcome { + text-align: center; + border-bottom: 1px solid $border-color; + + .blank-state-text { + margin-bottom: 0; + } +} + .blank-state { padding-top: 20px; padding-bottom: 20px; @@ -6,7 +15,15 @@ .blank-state-no-icon { padding-top: 40px; - padding-bottom: 40px; + padding-bottom: 40px; +} + +.blank-state-icon { + padding-bottom: 20px; + + path { + fill: $gray-darkest; + } } .blank-state-title { @@ -21,3 +38,7 @@ margin-bottom: $gl-padding; font-size: 15px; } + +.blank-state-welcome-title { + font-size: 24px; +} diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 41e77a4ac68..24b1ebab4b0 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -16,6 +16,9 @@ font-weight: normal; font-size: 16px; line-height: 36px; + &.diff-collapsed { + cursor: pointer; + } } .row-content-block { diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index a951a2b97fe..0c21d0240b3 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -1,8 +1,8 @@ .flash-container { cursor: pointer; margin: 0; + margin-bottom: $gl-padding; font-size: 14px; - width: 100%; z-index: 100; .flash-notice { @@ -18,9 +18,27 @@ } .flash-notice, .flash-alert { - .container-fluid.flash-text { + border-radius: $border-radius-default; + + .container-fluid.container-limited.flash-text { background: transparent; } } + + &.flash-container-page { + margin-bottom: 0; + + .flash-notice, .flash-alert { + border-radius: 0; + } + } +} + +@media (max-width: $screen-md-min) { + ul.notes { + .flash-container.timeline-content { + margin-left: 0; + } + } } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index aed0b44d91b..2c40ec430ca 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -175,6 +175,12 @@ ul.content-list { .panel > .content-list > li { padding: $gl-padding-top $gl-padding; + + &.commit { + @media (min-width: $screen-sm-min) { + padding-left: 46px + $gl-padding; + } + } } ul.controls { diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 02ea98e9d94..364952d3b4a 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -77,10 +77,10 @@ &.sub-nav { text-align: center; - background-color: $background-color; + background-color: $dark-background-color; .container-fluid { - background-color: $background-color; + background-color: $dark-background-color; margin-bottom: 0; } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 188823054fd..1a2220f3b40 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -3,6 +3,12 @@ padding-bottom: 25px; transition: padding $sidebar-transition-duration; + &.page-sidebar-pinned { + .sidebar-wrapper { + @include box-shadow(none); + } + } + .sidebar-wrapper { position: fixed; top: 0; @@ -11,6 +17,7 @@ height: 100%; overflow: hidden; transition: width $sidebar-transition-duration; + @include box-shadow(2px 0 16px 0 #bbb); } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 211a9af2348..4337fab5d87 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -12,10 +12,11 @@ $sidebar-breakpoint: 1024px; /* * UI elements */ -$border-color: #e5e5e5; -$focus-border-color: #3aabf0; -$table-border-color: #f0f0f0; -$background-color: #fafafa; +$border-color: #e5e5e5; +$focus-border-color: #3aabf0; +$table-border-color: #f0f0f0; +$background-color: #fafafa; +$dark-background-color: #f7f7f7; /* * Text @@ -153,9 +154,6 @@ $warning-message-bg: #fbf2d9; $warning-message-color: #9e8e60; $warning-message-border: #f0e2bb; -/* header */ -$light-grey-header: #faf9f9; - /* tanuki logo colors */ $tanuki-red: #e24329; $tanuki-orange: #fc6d26; diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 4e35ca329e4..0e4d8c140aa 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -63,8 +63,8 @@ form.edit-issue { .merge-request, .issue { &.today { - background: #efe; - border-color: #cec; + background: #f8feef; + border-color: #e1e8d5; } &.closed { diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index cbdf2859898..23ba83aba0e 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -87,6 +87,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :version_check_enabled, :admin_notification_email, :user_oauth_applications, + :user_default_external, :shared_runners_enabled, :shared_runners_text, :max_artifacts_size, diff --git a/app/controllers/concerns/diff_for_path.rb b/app/controllers/concerns/diff_for_path.rb new file mode 100644 index 00000000000..e09b8789eb2 --- /dev/null +++ b/app/controllers/concerns/diff_for_path.rb @@ -0,0 +1,25 @@ +module DiffForPath + extend ActiveSupport::Concern + + def render_diff_for_path(diffs, diff_refs, project) + diff_file = safe_diff_files(diffs, diff_refs: diff_refs, repository: project.repository).find do |diff| + diff.old_path == params[:old_path] && diff.new_path == params[:new_path] + end + + return render_404 unless diff_file + + diff_commit = commit_for_diff(diff_file) + blob = diff_file.blob(diff_commit) + @expand_all_diffs = true + + locals = { + diff_file: diff_file, + diff_commit: diff_commit, + diff_refs: diff_refs, + blob: blob, + project: project + } + + render json: { html: view_to_html_string('projects/diffs/_content', locals) } + end +end diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index f11c8321464..7241949393b 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -23,10 +23,9 @@ class Projects::ArtifactsController < Projects::ApplicationController entry = build.artifacts_metadata_entry(params[:path]) if entry.exists? - render json: { archive: build.artifacts_file.path, - entry: Base64.encode64(entry.path) } + send_artifacts_entry(build, entry) else - render json: {}, status: 404 + render_404 end end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 37d6521026c..727e84b40a1 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -3,6 +3,7 @@ # Not to be confused with CommitsController, plural. class Projects::CommitController < Projects::ApplicationController include CreatesCommit + include DiffForPath include DiffHelper # Authorize @@ -11,29 +12,14 @@ class Projects::CommitController < Projects::ApplicationController before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds] before_action :authorize_read_commit_status!, only: [:builds] before_action :commit - before_action :define_show_vars, only: [:show, :builds] + before_action :define_commit_vars, only: [:show, :diff_for_path, :builds] + before_action :define_status_vars, only: [:show, :builds] + before_action :define_note_vars, only: [:show, :diff_for_path] before_action :authorize_edit_tree!, only: [:revert, :cherry_pick] def show apply_diff_view_cookie! - @grouped_diff_notes = commit.notes.grouped_diff_notes - @notes = commit.notes.non_diff_notes.fresh - - Banzai::NoteRenderer.render( - @grouped_diff_notes.values.flatten + @notes, - @project, - current_user, - ) - - @note = @project.build_commit_note(commit) - - @noteable = @commit - @comments_target = { - noteable_type: 'Commit', - commit_id: @commit.id - } - respond_to do |format| format.html format.diff { render text: @commit.to_diff } @@ -41,6 +27,10 @@ class Projects::CommitController < Projects::ApplicationController end end + def diff_for_path + render_diff_for_path(@diffs, @commit.diff_refs, @project) + end + def builds end @@ -114,7 +104,7 @@ class Projects::CommitController < Projects::ApplicationController @ci_builds ||= Ci::Build.where(pipeline: pipelines) end - def define_show_vars + def define_commit_vars return git_not_found! unless commit opts = diff_options @@ -122,7 +112,28 @@ class Projects::CommitController < Projects::ApplicationController @diffs = commit.diffs(opts) @notes_count = commit.notes.count + end + + def define_note_vars + @grouped_diff_notes = commit.notes.grouped_diff_notes + @notes = commit.notes.non_diff_notes.fresh + + Banzai::NoteRenderer.render( + @grouped_diff_notes.values.flatten + @notes, + @project, + current_user, + ) + + @note = @project.build_commit_note(commit) + + @noteable = @commit + @comments_target = { + noteable_type: 'Commit', + commit_id: @commit.id + } + end + def define_status_vars @statuses = CommitStatus.where(pipeline: pipelines) @builds = Ci::Build.where(pipeline: pipelines) end diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index d240b9fe989..5f3ee71444d 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -1,29 +1,51 @@ require 'addressable/uri' class Projects::CompareController < Projects::ApplicationController + include DiffForPath include DiffHelper # Authorize before_action :require_non_empty_project before_action :authorize_download_code! - before_action :assign_ref_vars, only: [:index, :show] + before_action :define_ref_vars, only: [:index, :show, :diff_for_path] + before_action :define_diff_vars, only: [:show, :diff_for_path] before_action :merge_request, only: [:index, :show] def index end def show - compare = CompareService.new. - execute(@project, @head_ref, @project, @start_ref, diff_options) + end + + def diff_for_path + return render_404 unless @compare + + render_diff_for_path(@diffs, @diff_refs, @project) + end + + def create + redirect_to namespace_project_compare_path(@project.namespace, @project, + params[:from], params[:to]) + end + + private - if compare - @commits = Commit.decorate(compare.commits, @project) + def define_ref_vars + @start_ref = Addressable::URI.unescape(params[:from]) + @ref = @head_ref = Addressable::URI.unescape(params[:to]) + end + + def define_diff_vars + @compare = CompareService.new.execute(@project, @head_ref, @project, @start_ref) + + if @compare + @commits = Commit.decorate(@compare.commits, @project) @start_commit = @project.commit(@start_ref) @commit = @project.commit(@head_ref) @base_commit = @project.merge_base_commit(@start_ref, @head_ref) - @diffs = compare.diffs(diff_options) + @diffs = @compare.diffs(diff_options) @diff_refs = Gitlab::Diff::DiffRefs.new( base_sha: @base_commit.try(:sha), start_sha: @start_commit.try(:sha), @@ -35,18 +57,6 @@ class Projects::CompareController < Projects::ApplicationController end end - def create - redirect_to namespace_project_compare_path(@project.namespace, @project, - params[:from], params[:to]) - end - - private - - def assign_ref_vars - @start_ref = Addressable::URI.unescape(params[:from]) - @ref = @head_ref = Addressable::URI.unescape(params[:to]) - end - def merge_request @merge_request ||= @project.merge_requests.opened. find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index df1943dd9bb..941d68cda17 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -1,5 +1,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController include ToggleSubscriptionAction + include DiffForPath include DiffHelper include IssuableActions include ToggleAwardEmoji @@ -9,10 +10,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check, :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip ] - before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds] before_action :define_show_vars, only: [:show, :diffs, :commits, :builds] before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check] + before_action :define_commit_vars, only: [:diffs] + before_action :define_diff_comment_vars, only: [:diffs] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds] # Allow read any merge_request @@ -53,12 +55,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def show - @note_counts = Note.where(commit_id: @merge_request.commits.map(&:id)). - group(:commit_id).count - respond_to do |format| format.html - + format.json do render json: @merge_request end @@ -80,25 +79,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def diffs apply_diff_view_cookie! - @commit = @merge_request.diff_head_commit - @base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit - - @comments_target = { - noteable_type: 'MergeRequest', - noteable_id: @merge_request.id - } - - @use_legacy_diff_notes = !@merge_request.support_new_diff_notes? - @grouped_diff_notes = @merge_request.notes.grouped_diff_notes - - Banzai::NoteRenderer.render( - @grouped_diff_notes.values.flatten, - @project, - current_user, - @path, - @project_wiki, - @ref - ) + @merge_request_diff = @merge_request.merge_request_diff respond_to do |format| format.html @@ -106,10 +87,37 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + # With an ID param, loads the MR at that ID. Otherwise, accepts the same params as #new + # and uses that (unsaved) MR. + # + def diff_for_path + if params[:id] + merge_request + define_diff_comment_vars + else + build_merge_request + @diff_notes_disabled = true + @grouped_diff_notes = {} + end + + define_commit_vars + diffs = @merge_request.diffs(diff_options) + + render_diff_for_path(diffs, @merge_request.diff_refs, @merge_request.project) + end + def commits respond_to do |format| format.html { render 'show' } - format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_commits') } } + format.json do + # Get commits from repository + # or from cache if already merged + @commits = @merge_request.commits + @note_counts = Note.where(commit_id: @commits.map(&:id)). + group(:commit_id).count + + render json: { html: view_to_html_string('projects/merge_requests/show/_commits') } + end end end @@ -121,8 +129,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def new - params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) - @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute + build_merge_request @noteable = @merge_request @target_branches = if @merge_request.target_project @@ -308,10 +315,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController alias_method :issuable, :merge_request alias_method :awardable, :merge_request - def closes_issues - @closes_issues ||= @merge_request.closes_issues - end - def authorize_update_merge_request! return render_404 unless can?(current_user, :update_merge_request, @merge_request) end @@ -340,14 +343,33 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def define_show_vars + @noteable = @merge_request + @commits_count = @merge_request.commits.count + + @pipeline = @merge_request.pipeline + @statuses = @pipeline.statuses if @pipeline + + if @merge_request.locked_long_ago? + @merge_request.unlock_mr + @merge_request.close + end + + if request.format == :html || action_name == 'show' + define_show_html_vars + end + end + + # Discussion tab data is only required on html requests + def define_show_html_vars # Build a note object for comment form - @note = @project.notes.new(noteable: @merge_request) + @note = @project.notes.new(noteable: @noteable) - @discussions = @merge_request.mr_and_commit_notes. + @discussions = @noteable.mr_and_commit_notes. inc_author_project_award_emoji. fresh. discussions + # This is not executed lazily @notes = Banzai::NoteRenderer.render( @discussions.flatten, @project, @@ -356,28 +378,35 @@ class Projects::MergeRequestsController < Projects::ApplicationController @project_wiki, @ref ) - - @noteable = @merge_request - - # Get commits from repository - # or from cache if already merged - @commits = @merge_request.commits - - @merge_request_diff = @merge_request.merge_request_diff - - @pipeline = @merge_request.pipeline - @statuses = @pipeline.statuses if @pipeline - - if @merge_request.locked_long_ago? - @merge_request.unlock_mr - @merge_request.close - end end def define_widget_vars @pipeline = @merge_request.pipeline @pipelines = [@pipeline].compact - closes_issues + end + + def define_commit_vars + @commit = @merge_request.diff_head_commit + @base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit + end + + def define_diff_comment_vars + @comments_target = { + noteable_type: 'MergeRequest', + noteable_id: @merge_request.id + } + + @use_legacy_diff_notes = !@merge_request.support_new_diff_notes? + @grouped_diff_notes = @merge_request.notes.grouped_diff_notes + + Banzai::NoteRenderer.render( + @grouped_diff_notes.values.flatten, + @project, + current_user, + @path, + @project_wiki, + @ref + ) end def invalid_mr @@ -408,4 +437,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController params[:merge_when_build_succeeds].present? && @merge_request.pipeline && @merge_request.pipeline.active? end + + def build_merge_request + params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) + @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute + end end diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index efa7bf14d0f..80dad758afa 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -2,12 +2,14 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController # Authorize before_action :require_non_empty_project before_action :authorize_admin_project! + before_action :load_protected_branch, only: [:show, :update, :destroy] layout "project_settings" def index - @branches = @project.protected_branches.to_a + @protected_branches = @project.protected_branches.order(:name).page(params[:page]) @protected_branch = @project.protected_branches.new + gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } }) end def create @@ -16,26 +18,24 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController @project) end - def update - protected_branch = @project.protected_branches.find(params[:id]) - - if protected_branch && - protected_branch.update_attributes( - developers_can_push: params[:developers_can_push] - ) + def show + @matching_branches = @protected_branch.matching(@project.repository.branches) + end + def update + if @protected_branch && @protected_branch.update_attributes(protected_branch_params) respond_to do |format| - format.json { render json: protected_branch, status: :ok } + format.json { render json: @protected_branch, status: :ok } end else respond_to do |format| - format.json { render json: protected_branch.errors, status: :unprocessable_entity } + format.json { render json: @protected_branch.errors, status: :unprocessable_entity } end end end def destroy - @project.protected_branches.find(params[:id]).destroy + @protected_branch.destroy respond_to do |format| format.html { redirect_to namespace_project_protected_branches_path } @@ -45,6 +45,10 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController private + def load_protected_branch + @protected_branch = @project.protected_branches.find(params[:id]) + end + def protected_branch_params params.require(:protected_branch).permit(:name, :developers_can_push) end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 12e0d5a8413..1803aa8eab4 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -53,11 +53,11 @@ class ProjectsController < Projects::ApplicationController notice: "Project '#{@project.name}' was successfully updated." ) end - format.js else format.html { render 'edit' } - format.js end + + format.js end end diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index f240584ccbf..950f323e383 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -31,7 +31,7 @@ module AppearancesHelper end end - def navbar_icon(icon_name) - render "shared/icons/#{icon_name}.svg" + def navbar_icon(icon_name, size: 16) + render "shared/icons/#{icon_name}.svg", size: size end end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index b61266465e2..adab901700c 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -8,6 +8,10 @@ module DiffHelper [marked_old_line, marked_new_line] end + def expand_all_diffs? + @expand_all_diffs || params[:expand_all_diffs].present? + end + def diff_view diff_views = %w(inline parallel) @@ -18,16 +22,14 @@ module DiffHelper end end - def diff_hard_limit_enabled? - params[:force_show_diff].present? - end - def diff_options - options = { ignore_whitespace_change: hide_whitespace? } - if diff_hard_limit_enabled? - options.merge!(Commit.max_diff_options) + default_options = Commit.max_diff_options + + if action_name == 'diff_for_path' + default_options[:paths] = params.values_at(:old_path, :new_path) end - options + + default_options.merge(ignore_whitespace_change: hide_whitespace?) end def safe_diff_files(diffs, diff_refs: nil, repository: nil) @@ -90,7 +92,7 @@ module DiffHelper def commit_for_diff(diff_file) return diff_file.content_commit if diff_file.content_commit - + if diff_file.deleted_file @base_commit || @commit.parent || @commit else diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index c7dedfe9254..db6e731c744 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -55,6 +55,10 @@ module MergeRequestsHelper end.sort.to_sentence end + def mr_closes_issues + @mr_closes_issues ||= @merge_request.closes_issues + end + def mr_change_branches_path(merge_request) new_namespace_project_merge_request_path( @project.namespace, @project, @@ -92,4 +96,8 @@ module MergeRequestsHelper ["#{source_path}:#{source_branch}", "#{target_path}:#{target_branch}"] end end + + def merge_request_button_visibility(merge_request, closed) + return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) + end end diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb index b04b0a5114c..8cb82c2d5cc 100644 --- a/app/helpers/time_helper.rb +++ b/app/helpers/time_helper.rb @@ -23,4 +23,11 @@ module TimeHelper def date_from_to(from, to) "#{from.to_s(:short)} - #{to.to_s(:short)}" end + + def duration_in_numbers(finished_at, started_at) + diff_in_seconds = finished_at.to_i - started_at.to_i + time_format = diff_in_seconds < 1.hour ? "%M:%S" : "%H:%M:%S" + + Time.at(diff_in_seconds).utc.strftime(time_format) + end end diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb index 65598ad9ed3..d887cdadc34 100644 --- a/app/helpers/workhorse_helper.rb +++ b/app/helpers/workhorse_helper.rb @@ -28,4 +28,10 @@ module WorkhorseHelper headers.store(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format)) head :ok end + + # Send an entry from artifacts through Workhorse + def send_artifacts_entry(build, entry) + headers.store(*Gitlab::Workhorse.send_artifacts_entry(build, entry)) + head :ok + end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 7bf618d60b9..c6f77cc055f 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -142,6 +142,7 @@ class ApplicationSetting < ActiveRecord::Base send_user_confirmation_email: false, container_registry_token_expire_delay: 5, repository_storage: 'default', + user_default_external: false, ) end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5c973749975..e189dbac285 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -13,6 +13,7 @@ module Ci scope :ignore_failures, ->() { where(allow_failure: false) } scope :with_artifacts, ->() { where.not(artifacts_file: nil) } scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } + scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader @@ -25,10 +26,6 @@ module Ci after_create :execute_hooks class << self - def last_month - where('created_at > ?', Date.today - 1.month) - end - def first_pending pending.unstarted.order('created_at ASC').first end diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 4066958f67c..630ee9601e0 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -23,7 +23,7 @@ class CommitRange attr_reader :commit_from, :notation, :commit_to attr_reader :ref_from, :ref_to - # Optional Project model + # The Project model attr_accessor :project # The beginning and ending refs can be named or SHAs, and @@ -56,7 +56,7 @@ class CommitRange # Initialize a CommitRange # # range_string - The String commit range. - # project - An optional Project model. + # project - The Project model. # # Raises ArgumentError if `range_string` does not match `PATTERN`. def initialize(range_string, project) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 083e93f1ee7..157901378d3 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -19,7 +19,7 @@ class MergeRequest < ActiveRecord::Base after_create :create_merge_request_diff, unless: :importing? after_update :update_merge_request_diff - delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil + delegate :commits, :real_size, to: :merge_request_diff, prefix: nil # When this attribute is true some MR validation is ignored # It allows us to close or modify broken merge requests @@ -164,6 +164,10 @@ class MergeRequest < ActiveRecord::Base merge_request_diff ? merge_request_diff.first_commit : compare_commits.first end + def diffs(*args) + merge_request_diff ? merge_request_diff.diffs(*args) : compare.diffs(*args) + end + def diff_size merge_request_diff.size end @@ -318,11 +322,11 @@ class MergeRequest < ActiveRecord::Base end def merge_event - self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last + @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last end def closed_event - self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last + @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last end WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index ba235750aeb..feaba925bad 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -46,7 +46,8 @@ class MergeRequestDiff < ActiveRecord::Base compare.diffs(options) end else - @diffs ||= load_diffs(st_diffs, options) + @diffs ||= {} + @diffs[options] ||= load_diffs(st_diffs, options) end end @@ -144,6 +145,12 @@ class MergeRequestDiff < ActiveRecord::Base def load_diffs(raw, options) if raw.respond_to?(:each) + if paths = options[:paths] + raw = raw.select do |diff| + paths.include?(diff[:old_path]) || paths.include?(diff[:new_path]) + end + end + Gitlab::Git::DiffCollection.new(raw, options) else Gitlab::Git::DiffCollection.new([]) diff --git a/app/models/project.rb b/app/models/project.rb index 029026a4e56..a66b750cd48 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -425,8 +425,8 @@ class Project < ActiveRecord::Base container_registry_repository.tags.any? end - def commit(id = 'HEAD') - repository.commit(id) + def commit(ref = 'HEAD') + repository.commit(ref) end def merge_base_commit(first_commit_id, second_commit_id) @@ -802,18 +802,12 @@ class Project < ActiveRecord::Base @repo_exists = false end + # Branches that are not _exactly_ matched by a protected branch. def open_branches - # We're using a Set here as checking values in a large Set is faster than - # checking values in a large Array. - protected_set = Set.new(protected_branch_names) - - repository.branches.reject do |branch| - protected_set.include?(branch.name) - end - end - - def protected_branch_names - @protected_branch_names ||= protected_branches.pluck(:name) + exact_protected_branch_names = protected_branches.reject(&:wildcard?).map(&:name) + branch_names = repository.branches.map(&:name) + non_open_branch_names = Set.new(exact_protected_branch_names).intersection(Set.new(branch_names)) + repository.branches.reject { |branch| non_open_branch_names.include? branch.name } end def root_ref?(branch) @@ -830,11 +824,12 @@ class Project < ActiveRecord::Base # Check if current branch name is marked as protected in the system def protected_branch?(branch_name) - protected_branch_names.include?(branch_name) + @protected_branches ||= self.protected_branches.to_a + ProtectedBranch.matching(branch_name, protected_branches: @protected_branches).present? end def developers_can_push_to_protected_branch?(branch_name) - protected_branches.any? { |pb| pb.name == branch_name && pb.developers_can_push } + protected_branches.matching(branch_name).any?(&:developers_can_push) end def forked? diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index 58cb720c3c1..ce7d1c5d5b1 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -112,15 +112,7 @@ class IrkerService < Service # Authorize both irc://domain.com/#chan and irc://domain.com/chan if uri.is_a?(URI) && uri.scheme[/^ircs?\z/] && !uri.path.nil? - # Do not authorize irc://domain.com/ - if uri.fragment.nil? && uri.path.length > 1 - uri.to_s - else - # Authorize irc://domain.com/smthg#chan - # The irker daemon will deal with it by concatenating smthg and - # chan, thus sending messages on #smthgchan - uri.to_s - end + uri.to_s end end end diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 33cf046fa75..b7011d7afdf 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -8,4 +8,51 @@ class ProtectedBranch < ActiveRecord::Base def commit project.commit(self.name) end + + # Returns all protected branches that match the given branch name. + # This realizes all records from the scope built up so far, and does + # _not_ return a relation. + # + # This method optionally takes in a list of `protected_branches` to search + # through, to avoid calling out to the database. + def self.matching(branch_name, protected_branches: nil) + (protected_branches || all).select { |protected_branch| protected_branch.matches?(branch_name) } + end + + # Returns all branches (among the given list of branches [`Gitlab::Git::Branch`]) + # that match the current protected branch. + def matching(branches) + branches.select { |branch| self.matches?(branch.name) } + end + + # Checks if the protected branch matches the given branch name. + def matches?(branch_name) + return false if self.name.blank? + + exact_match?(branch_name) || wildcard_match?(branch_name) + end + + # Checks if this protected branch contains a wildcard + def wildcard? + self.name && self.name.include?('*') + end + + protected + + def exact_match?(branch_name) + self.name == branch_name + end + + def wildcard_match?(branch_name) + wildcard_regex === branch_name + end + + def wildcard_regex + @wildcard_regex ||= begin + name = self.name.gsub('*', 'STAR_DONT_ESCAPE') + quoted_name = Regexp.quote(name) + regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?') + /\A#{regex_string}\z/ + end + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index ba66bc47c29..5b670cb4b8f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -78,9 +78,9 @@ class Repository end end - def commit(id = 'HEAD') + def commit(ref = 'HEAD') return nil unless exists? - commit = Gitlab::Git::Commit.find(raw_repository, id) + commit = Gitlab::Git::Commit.find(raw_repository, ref) commit = ::Commit.new(commit, @project) if commit commit rescue Rugged::OdbError diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb index 016172c6d7e..f4bcb49b34d 100644 --- a/app/models/sent_notification.rb +++ b/app/models/sent_notification.rb @@ -72,6 +72,19 @@ class SentNotification < ActiveRecord::Base end end + def position=(new_position) + if new_position.is_a?(String) + new_position = JSON.parse(new_position) rescue nil + end + + if new_position.is_a?(Hash) + new_position = new_position.with_indifferent_access + new_position = Gitlab::Diff::Position.new(new_position) + end + + super(new_position) + end + def to_param self.reply_key end diff --git a/app/models/user.rb b/app/models/user.rb index 695a47ba6eb..79c670cb35a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -15,7 +15,7 @@ class User < ActiveRecord::Base add_authentication_token_field :authentication_token default_value_for :admin, false - default_value_for :external, false + default_value_for(:external) { current_application_settings.user_default_external } default_value_for :can_create_group, gitlab_config.default_can_create_group default_value_for :can_create_team, false default_value_for :hide_no_ssh_key, false diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index e2bccbdbcc3..149822aa647 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -3,7 +3,7 @@ require 'securerandom' # Compare 2 branches for one repo or between repositories # and return Gitlab::Git::Compare object that responds to commits and diffs class CompareService - def execute(source_project, source_branch, target_project, target_branch, diff_options = {}) + def execute(source_project, source_branch, target_project, target_branch) source_commit = source_project.commit(source_branch) return unless source_commit diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 21490ac77ea..b11ecd97a57 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -61,19 +61,14 @@ module MergeRequests merge_requests.each do |merge_request| if merge_request.source_branch == @branch_name || force_push? merge_request.reload_diff - merge_request.mark_as_unchecked else mr_commit_ids = merge_request.commits.map(&:id) push_commit_ids = @commits.map(&:id) matches = mr_commit_ids & push_commit_ids - - if matches.any? - merge_request.reload_diff - merge_request.mark_as_unchecked - else - merge_request.mark_as_unchecked - end + merge_request.reload_diff if matches.any? end + + merge_request.mark_as_unchecked end end diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml index 862b86d9d4a..dd2e7ebd030 100644 --- a/app/views/admin/abuse_reports/_abuse_report.html.haml +++ b/app/views/admin/abuse_reports/_abuse_report.html.haml @@ -3,14 +3,14 @@ %tr %td - if user - = link_to user.name, [:admin, user] + = link_to user.name, user .light.small Joined #{time_ago_with_tooltip(user.created_at)} - else (removed) %td - if reporter - = link_to reporter.name, [:admin, reporter] + = link_to reporter.name, reporter - else (removed) .light.small diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index eb325576e4f..8de28528cda 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -100,6 +100,13 @@ = f.label :user_oauth_applications do = f.check_box :user_oauth_applications Allow users to register any application to use GitLab as an OAuth provider + .form-group + = f.label :user_default_external, 'New users set to external', class: 'control-label col-sm-2' + .col-sm-10 + .checkbox + = f.label :user_default_external do + = f.check_box :user_default_external + Newly registered users will by default be external %fieldset %legend Sign-in Restrictions diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index fe0b9d3a491..3145212728f 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -44,7 +44,7 @@ %legend Access .form-group = f.label :projects_limit, class: 'control-label' - .col-sm-10= f.number_field :projects_limit, class: 'form-control' + .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control' .form-group = f.label :can_create_group, class: 'control-label' diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml index 9b838b9f3b7..6306fe6d0bf 100644 --- a/app/views/explore/snippets/index.html.haml +++ b/app/views/explore/snippets/index.html.haml @@ -10,7 +10,6 @@ - if current_user .pull-right = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do - = icon('plus') New Snippet .oneline diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml index cc8ea066cb9..3612f1ce5c6 100644 --- a/app/views/layouts/_flash.html.haml +++ b/app/views/layouts/_flash.html.haml @@ -1,4 +1,4 @@ -.flash-container +.flash-container.flash-container-page - if alert .flash-alert = alert diff --git a/app/views/profiles/_head.html.haml b/app/views/profiles/_head.html.haml new file mode 100644 index 00000000000..003884a5bd9 --- /dev/null +++ b/app/views/profiles/_head.html.haml @@ -0,0 +1,3 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/cropper.js') + = page_specific_javascript_tag('profile/application.js') diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 8efe486e01b..57d16d29158 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -1,4 +1,5 @@ - page_title "Account" += render 'profiles/head' - if current_user.ldap_user? .alert.alert-info diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml index 9c404b6935f..9fe86e6b291 100644 --- a/app/views/profiles/audit_log.html.haml +++ b/app/views/profiles/audit_log.html.haml @@ -1,4 +1,5 @@ - page_title "Audit Log" += render 'profiles/head' .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 6f7fefdb46d..dc499be885b 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -1,4 +1,5 @@ - page_title "Emails" += render 'profiles/head' .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml index b3ed59a1a4a..6ea358d9f63 100644 --- a/app/views/profiles/keys/_form.html.haml +++ b/app/views/profiles/keys/_form.html.haml @@ -4,7 +4,7 @@ .form-group = f.label :key, class: 'label-light' - = f.text_area :key, class: "form-control", rows: 8, required: true + = f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: "Don't paste the private part of the SSH key. Paste the public part, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'." .form-group = f.label :title, class: 'label-light' = f.text_field :title, class: "form-control", required: true diff --git a/app/views/profiles/keys/show.html.haml b/app/views/profiles/keys/show.html.haml index 89f6f01581a..6283ceebf10 100644 --- a/app/views/profiles/keys/show.html.haml +++ b/app/views/profiles/keys/show.html.haml @@ -1,2 +1,3 @@ - page_title @key.title, "SSH Keys" += render 'profiles/head' = render "key_details" diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index f77738f97f5..844fce59704 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -1,4 +1,5 @@ - page_title "Notifications" += render 'profiles/head' %div - if @user.errors.any? diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 1b45548bd02..71ac367830d 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -1,4 +1,5 @@ - page_title "Personal Access Tokens" += render 'profiles/head' .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 1b1b16d656f..b4d35dc9a3e 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -1,4 +1,5 @@ - page_title 'Preferences' += render 'profiles/head' = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'row prepend-top-default js-preferences-form'} do |f| .col-lg-3.profile-settings-sidebar diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index eef50d887c7..d9fa74fad90 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -1,3 +1,5 @@ += render 'profiles/head' + = form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit-user prepend-top-default" }, authenticity_token: true do |f| = form_errors(@user) diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 593be2617c1..5890456bee2 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -1,5 +1,6 @@ - page_title 'Two-Factor Authentication', 'Account' - header_title "Two-Factor Authentication", profile_two_factor_auth_path += render 'profiles/head' .row.prepend-top-default .col-lg-3 diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 4e801cc72fe..4421f3b9562 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -67,4 +67,4 @@ = render "sidebar" :javascript - new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}", "#{@build.status}", "#{trace_with_state[:state]}") + new Build("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}", "#{@build.status}", "#{trace_with_state[:state]}") diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index e38d1ff5ff0..af8dd5cd02c 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -45,7 +45,7 @@ %td - if pipeline.started_at && pipeline.finished_at %p.duration - #{duration_in_words(pipeline.finished_at, pipeline.started_at)} + = duration_in_numbers(pipeline.finished_at, pipeline.started_at) %td .controls.hidden-xs.pull-right diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml new file mode 100644 index 00000000000..0c0424edffd --- /dev/null +++ b/app/views/projects/diffs/_content.html.haml @@ -0,0 +1,29 @@ +.diff-content.diff-wrap-lines + - # Skip all non non-supported blobs + - return unless blob.respond_to?(:text?) + - if diff_file.too_large? + .nothing-here-block This diff could not be displayed because it is too large. + - elsif blob.only_display_raw? + .nothing-here-block This file is too large to display. + - elsif blob_text_viewable?(blob) + - if !project.repository.diffable?(blob) + .nothing-here-block This diff was suppressed by a .gitattributes entry. + - elsif diff_file.diff_lines.length > 0 + - if diff_file.collapsed_by_default? && !expand_all_diffs? + - url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path)) + .nothing-here-block.diff-collapsed{data: { diff_for_path: url } } + This diff is collapsed. Click to expand it. + - elsif diff_view == 'parallel' + = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob + - else + = render "projects/diffs/text_file", diff_file: diff_file + - else + - if diff_file.mode_changed? + .nothing-here-block File mode changed + - elsif diff_file.renamed_file + .nothing-here-block File moved + - elsif blob.image? + - old_blob = diff_file.old_blob(diff_commit) + = render "projects/diffs/image", diff_file: diff_file, old_file: old_blob, file: blob + - else + .nothing-here-block No preview for this file type diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 2a15c306f07..20aaab5accf 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -6,6 +6,8 @@ .content-block.oneline-block.files-changed .inline-parallel-buttons + - unless expand_all_diffs? + = link_to 'Expand all', url_for(params.merge(expand_all_diffs: 1, format: 'html')), class: 'btn btn-default' - if show_whitespace_toggle - if current_controller?(:commit) = commit_diff_whitespace_link(@project, @commit, class: 'hidden-xs') diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 3b758a1ec4e..c306909fb1a 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -16,28 +16,4 @@ = view_file_btn(diff_commit.id, diff_file, project) - .diff-content.diff-wrap-lines - - # Skip all non non-supported blobs - - return unless blob.respond_to?(:text?) - - if diff_file.too_large? - .nothing-here-block This diff could not be displayed because it is too large. - - elsif blob.only_display_raw? - .nothing-here-block This file is too large to display. - - elsif blob_text_viewable?(blob) - - if !project.repository.diffable?(blob) - .nothing-here-block This diff was suppressed by a .gitattributes entry. - - elsif diff_file.diff_lines.length > 0 - - if diff_view == 'parallel' - = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i - - else - = render "projects/diffs/text_file", diff_file: diff_file, index: i - - else - - if diff_file.mode_changed? - .nothing-here-block File mode changed - - elsif diff_file.renamed_file - .nothing-here-block File moved - - elsif blob.image? - - old_blob = diff_file.old_blob(diff_commit) - = render "projects/diffs/image", diff_file: diff_file, old_file: old_blob, file: blob, index: i - - else - .nothing-here-block No preview for this file type + = render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, diff_refs: diff_refs, blob: blob, project: project diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index 15536c17f8e..10fa1ddf2e5 100644 --- a/app/views/projects/diffs/_warning.html.haml +++ b/app/views/projects/diffs/_warning.html.haml @@ -2,9 +2,6 @@ %h4 Too many changes to show. .pull-right - - unless diff_hard_limit_enabled? - = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true, format: nil)), class: "btn btn-sm" - - if current_controller?(:commit) or current_controller?(:merge_requests) - if current_controller?(:commit) = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-sm" diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 7ce4c1e5555..312bd86ed04 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -6,21 +6,37 @@ - if current_user = auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues") -%div{ class: container_class } - .top-area - = render 'shared/issuable/nav', type: :issues - .nav-controls - - if current_user - = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do - = icon('rss') - %span.icon-label - Subscribe - = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) +%div{ class: (container_class) } + - if @project.issues.any? + .top-area + = render 'shared/issuable/nav', type: :issues + .nav-controls + - if current_user + = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do + = icon('rss') + %span.icon-label + Subscribe + = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) + - if can? current_user, :create_issue, @project + = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do + New Issue + = render 'shared/issuable/filter', type: :issues + + .issues-holder + = render "issues" + - else + .blank-state.blank-state-welcome + %h2.blank-state-title.blank-state-welcome-title + Welcome to GitLab Issues + %p.blank-state-text + Code, test, and deploy together + .blank-state + .blank-state-icon + = navbar_icon("issues", size: 50) + %h3.blank-state-title + You don't have any issues right now. + %p.blank-state-text + Issues are the best way to track your project progress - if can? current_user, :create_issue, @project - = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do New Issue - - = render 'shared/issuable/filter', type: :issues - - .issues-holder - = render "issues" diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 2ec96308fd7..873ed9b59ee 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -42,7 +42,7 @@ = succeed '.' do = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - - if @commits.present? + - if @commits_count.nonzero? %ul.merge-request-tabs.nav-links.no-top.no-bottom %li.notes-tab = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do @@ -51,7 +51,7 @@ %li.commits-tab = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits - %span.badge= @commits.size + %span.badge= @commits_count - if @pipeline %li.builds-tab = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#builds', action: 'builds', toggle: 'tab'} do diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 5bf5210aeab..b24bdf22ceb 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -19,13 +19,13 @@ Options .dropdown-menu.dropdown-menu-align-right.hidden-lg %ul - %li{ class: issue_button_visibility(@merge_request, true) } + %li{ class: merge_request_button_visibility(@merge_request, true) } = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request' - %li{ class: issue_button_visibility(@merge_request, false) } + %li{ class: merge_request_button_visibility(@merge_request, false) } = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' %li = link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit' - = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request' - = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request' + = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{merge_request_button_visibility(@merge_request, true)}", title: 'Close merge request' + = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen reopen-mr-link #{merge_request_button_visibility(@merge_request, false)}", title: 'Reopen merge request' = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit" do Edit diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 0e0af57d76e..dc18f715f25 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -22,10 +22,10 @@ - elsif @merge_request.can_be_merged? = render 'projects/merge_requests/widget/open/accept' - - if @closes_issues.present? + - if mr_closes_issues.present? .mr-widget-footer %span %i.fa.fa-check - Accepting this merge request will close #{"issue".pluralize(@closes_issues.size)} + Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)} = succeed '.' do - != markdown issues_sentence(@closes_issues), pipeline: :gfm, author: @merge_request.author + != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 1c39ce897a3..56d302fab82 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -2,6 +2,8 @@ = render "projects/notes/notes" %ul.notes.notes-form.timeline %li.timeline-entry + .flash-container.timeline-content + - if can? current_user, :create_note, @project .timeline-icon.hidden-xs.hidden-sm %a.author_link{ href: user_path(current_user) } diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index 565905cbe7b..97cb1a9052b 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -1,6 +1,6 @@ %h5.prepend-top-0 - Already Protected (#{@branches.size}) -- if @branches.empty? + Already Protected (#{@protected_branches.size}) +- if @protected_branches.empty? %p.settings-message.text-center No branches are protected, protect a branch with the form above. - else @@ -9,33 +9,18 @@ %table.table.protected-branches-list %colgroup %col{ width: "30%" } - %col{ width: "30%" } + %col{ width: "25%" } %col{ width: "25%" } - if can_admin_project %col %thead %tr - %th Branch - %th Last commit - %th Developers can push + %th Protected Branch + %th Commit + %th Developers Can Push - if can_admin_project %th %tbody - - @branches.each do |branch| - - @url = namespace_project_protected_branch_path(@project.namespace, @project, branch) - %tr - %td - = link_to(branch.name, namespace_project_commits_path(@project.namespace, @project, branch.name)) - - if @project.root_ref?(branch.name) - %span.label.label-info.prepend-left-5 default - %td - - if commit = branch.commit - = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id') - #{time_ago_with_tooltip(commit.committed_date)} - - else - (branch was removed from repository) - %td - = check_box_tag("developers_can_push", branch.id, branch.developers_can_push, data: { url: @url }) - - if can_admin_project - %td - = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning btn-sm" + = render partial: @protected_branches, locals: { can_admin_project: can_admin_project } + + = paginate @protected_branches, theme: 'gitlab' diff --git a/app/views/projects/protected_branches/_dropdown.html.haml b/app/views/projects/protected_branches/_dropdown.html.haml new file mode 100644 index 00000000000..b803d932e67 --- /dev/null +++ b/app/views/projects/protected_branches/_dropdown.html.haml @@ -0,0 +1,17 @@ += f.hidden_field(:name) + += dropdown_tag("Protected Branch", + options: { title: "Pick protected branch", toggle_class: 'js-protected-branch-select js-filter-submit', + filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search protected branches", + footer_content: true, + data: { show_no: true, show_any: true, show_upcoming: true, + selected: params[:protected_branch_name], + project_id: @project.try(:id) } }) do + + %ul.dropdown-footer-list.hidden.protected-branch-select-footer-list + %li + = link_to '#', title: "New Protected Branch", class: "create-new-protected-branch" do + Create new + +:javascript + new ProtectedBranchSelect(); diff --git a/app/views/projects/protected_branches/_matching_branch.html.haml b/app/views/projects/protected_branches/_matching_branch.html.haml new file mode 100644 index 00000000000..8a5332ca5bb --- /dev/null +++ b/app/views/projects/protected_branches/_matching_branch.html.haml @@ -0,0 +1,9 @@ +%tr + %td + = link_to matching_branch.name, namespace_project_tree_path(@project.namespace, @project, matching_branch.name) + - if @project.root_ref?(matching_branch.name) + %span.label.label-info.prepend-left-5 default + %td + - commit = @project.commit(matching_branch.name) + = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id') + = time_ago_with_tooltip(commit.committed_date) diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml new file mode 100644 index 00000000000..474aec3a97c --- /dev/null +++ b/app/views/projects/protected_branches/_protected_branch.html.haml @@ -0,0 +1,21 @@ +- url = namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) +%tr + %td + = protected_branch.name + - if @project.root_ref?(protected_branch.name) + %span.label.label-info.prepend-left-5 default + %td + - if protected_branch.wildcard? + - matching_branches = protected_branch.matching(repository.branches) + = link_to pluralize(matching_branches.count, "matching branch"), namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) + - else + - if commit = protected_branch.commit + = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id') + = time_ago_with_tooltip(commit.committed_date) + - else + (branch was removed from repository) + %td + = check_box_tag("developers_can_push", protected_branch.id, protected_branch.developers_can_push, data: { url: url }) + - if can_admin_project + %td + = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning btn-sm pull-right" diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index c7d317dbaee..5669713d9a1 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -4,30 +4,38 @@ .col-lg-3 %h4.prepend-top-0 = page_title - %p Keep stable branches secure and force developers to use Merge Requests - .col-lg-9 - %h5.prepend-top-0 - Protect a branch - .account-well.append-bottom-default - %p.light-header.append-bottom-0 Protected branches are designed to + %p Keep stable branches secure and force developers to use merge requests. + %p.prepend-top-20 + Protected branches are designed to: %ul %li prevent pushes from everybody except #{link_to "masters", help_page_path("permissions", "permissions"), class: "vlink"} %li prevent anyone from force pushing to the branch %li prevent anyone from deleting the branch %p.append-bottom-0 Read more about #{link_to "project permissions", help_page_path("permissions", "permissions"), class: "underlined-link"} + .col-lg-9 + %h5.prepend-top-0 + Protect a branch - if can? current_user, :admin_project, @project = form_for [@project.namespace.becomes(Namespace), @project, @protected_branch] do |f| = form_errors(@protected_branch) .form-group = f.label :name, "Branch", class: "label-light" - = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: true}, {class: "select2", data: {placeholder: "Select branch"}}) + = render partial: "dropdown", locals: { f: f } + %p.help-block + = link_to "Wildcards", help_page_path(category: 'workflow', file: 'protected_branches', format: 'md', anchor: "wildcard-protected-branches") + such as + %code *-stable + or + %code production/* + are supported. + .form-group = f.check_box :developers_can_push, class: "pull-left" .prepend-left-20 = f.label :developers_can_push, "Developers can push", class: "label-light append-bottom-0" %p.light.append-bottom-0 Allow developers to push to this branch - = f.submit "Protect", class: "btn-create btn" + = f.submit "Protect", class: "btn-create btn protect-branch-btn", disabled: true %hr = render "branches_list" diff --git a/app/views/projects/protected_branches/show.html.haml b/app/views/projects/protected_branches/show.html.haml new file mode 100644 index 00000000000..4d8169815b3 --- /dev/null +++ b/app/views/projects/protected_branches/show.html.haml @@ -0,0 +1,25 @@ +- page_title @protected_branch.name, "Protected Branches" + +.row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0 + = @protected_branch.name + + .col-lg-9 + %h5 Matching Branches + - if @matching_branches.present? + .table-responsive + %table.table.protected-branches-list + %colgroup + %col{ width: "30%" } + %col{ width: "30%" } + %thead + %tr + %th Branch + %th Last commit + %tbody + - @matching_branches.each do |matching_branch| + = render partial: "matching_branch", object: matching_branch + - else + %p.settings-message.text-center + Couldn't find any matching branches. diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index bf57beb9d07..bdbf3e5f4d6 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -1,27 +1,29 @@ .hidden-xs - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New Snippet" do - = icon('plus') - New Snippet + - if can?(current_user, :create_project_snippet, @project) + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New Snippet" do + New Snippet - if can?(current_user, :update_project_snippet, @snippet) = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do Edit - if can?(current_user, :update_project_snippet, @snippet) = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-warning", title: 'Delete Snippet' do Delete -.visible-xs-block.dropdown - %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } - Options - %span.caret - .dropdown-menu.dropdown-menu-full-width - %ul - %li - = link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New Snippet" do - New Snippet - - if can?(current_user, :update_project_snippet, @snippet) - %li - = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do - Edit - - if can?(current_user, :update_project_snippet, @snippet) - %li - = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do - Delete +- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) + .visible-xs-block.dropdown + %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } + Options + %span.caret + .dropdown-menu.dropdown-menu-full-width + %ul + - if can?(current_user, :create_project_snippet, @project) + %li + = link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New Snippet" do + New Snippet + - if can?(current_user, :update_project_snippet, @snippet) + %li + = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do + Edit + - if can?(current_user, :update_project_snippet, @snippet) + %li + = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do + Delete diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index 96fee3b17b2..6c994ae486b 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -2,9 +2,9 @@ .row-content-block.top-block .pull-right - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do - = icon('plus') - New Snippet + - if can?(current_user, :create_project_snippet, @project) + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do + New Snippet .oneline Share code pastes with others out of git repository diff --git a/app/views/shared/icons/_issues.svg b/app/views/shared/icons/_issues.svg deleted file mode 100644 index 2682c27ade9..00000000000 --- a/app/views/shared/icons/_issues.svg +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch --> - <title>Group</title> - <desc>Created with Sketch.</desc> - <defs></defs> - <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> - <g id="Group" fill="#7E7C7C"> - <path d="M8,0 C3.581,0 0,3.581 0,8 C0,12.419 3.581,16 8,16 C12.419,16 16,12.419 16,8 C16,3.581 12.419,0 8,0 M8,2 C11.308,2 14,4.692 14,8 C14,11.308 11.308,14 8,14 C4.692,14 2,11.308 2,8 C2,4.692 4.692,2 8,2" id="Fill-1"></path> - <path d="M7.1597,4 L8.8887,4 L8.8887,8 L7.1107,8 L7.1597,4 Z M7.1597,9.6667 L8.8887,9.6667 L8.8887,11.4447 L7.1107,11.4447 L7.1597,9.6667 Z" id="Combined-Shape"></path> - </g> - </g> -</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_issues.svg.erb b/app/views/shared/icons/_issues.svg.erb new file mode 100644 index 00000000000..fa8655b5609 --- /dev/null +++ b/app/views/shared/icons/_issues.svg.erb @@ -0,0 +1,4 @@ +<svg width="<%= size %>" height="<%= size %>" viewBox="0 0 16 16" class="gitlab-icon"> + <path fill="#7E7C7C" d="M8,0 C3.581,0 0,3.581 0,8 C0,12.419 3.581,16 8,16 C12.419,16 16,12.419 16,8 C16,3.581 12.419,0 8,0 M8,2 C11.308,2 14,4.692 14,8 C14,11.308 11.308,14 8,14 C4.692,14 2,11.308 2,8 C2,4.692 4.692,2 8,2"></path> + <path fill="#7E7C7C" d="M7.1597,4 L8.8887,4 L8.8887,8 L7.1107,8 L7.1597,4 Z M7.1597,9.6667 L8.8887,9.6667 L8.8887,11.4447 L7.1107,11.4447 L7.1597,9.6667 Z"></path> +</svg> diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index a7769654b61..160c6cd84da 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -1,27 +1,28 @@ .hidden-xs - = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New Snippet" do - = icon('plus') - New Snippet + - if current_user + = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New Snippet" do + New Snippet - if can?(current_user, :update_personal_snippet, @snippet) = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do Edit - if can?(current_user, :admin_personal_snippet, @snippet) - = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-warning", title: 'Delete Snippet' do + = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do Delete -.visible-xs-block.dropdown - %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } - Options - %span.caret - .dropdown-menu.dropdown-menu-full-width - %ul - %li - = link_to new_snippet_path, title: "New Snippet" do - New Snippet - - if can?(current_user, :update_personal_snippet, @snippet) +- if current_user + .visible-xs-block.dropdown + %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } + Options + %span.caret + .dropdown-menu.dropdown-menu-full-width + %ul %li - = link_to edit_snippet_path(@snippet) do - Edit - - if can?(current_user, :admin_personal_snippet, @snippet) - %li - = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do - Delete + = link_to new_snippet_path, title: "New Snippet" do + New Snippet + - if can?(current_user, :update_personal_snippet, @snippet) + %li + = link_to edit_snippet_path(@snippet) do + Edit + - if can?(current_user, :admin_personal_snippet, @snippet) + %li + = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do + Delete diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 68665858c3e..db2b4885861 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -29,6 +29,11 @@ = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do = icon('rss') + - if current_user.admin? + + = link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area', + data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('users') .avatar-holder = link_to avatar_icon(@user, 400), target: '_blank' do diff --git a/config/application.rb b/config/application.rb index 2b0595ede2b..21e7cc7b6e8 100644 --- a/config/application.rb +++ b/config/application.rb @@ -84,6 +84,7 @@ module Gitlab config.assets.precompile << "graphs/application.js" config.assets.precompile << "users/application.js" config.assets.precompile << "network/application.js" + config.assets.precompile << "profile/application.js" config.assets.precompile << "lib/utils/*.js" config.assets.precompile << "lib/*.js" diff --git a/config/routes.rb b/config/routes.rb index 1572656b8c5..b4f83c58bbd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -615,10 +615,18 @@ Rails.application.routes.draw do post :retry_builds post :revert post :cherry_pick + get :diff_for_path end end - resources :compare, only: [:index, :create] + resources :compare, only: [:index, :create] do + collection do + get :diff_for_path + end + end + + get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ } + resources :network, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } resources :graphs, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } do @@ -629,9 +637,6 @@ Rails.application.routes.draw do end end - get '/compare/:from...:to' => 'compare#show', :as => 'compare', - :constraints => { from: /.+/, to: /.+/ } - resources :snippets, constraints: { id: /\d+/ } do member do get 'raw' @@ -706,12 +711,14 @@ Rails.application.routes.draw do post :toggle_subscription post :toggle_award_emoji post :remove_wip + get :diff_for_path end collection do get :branch_from get :branch_to get :update_branches + get :diff_for_path end end @@ -720,7 +727,7 @@ Rails.application.routes.draw do resource :release, only: [:edit, :update] end - resources :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } + resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } resources :variables, only: [:index, :show, :update, :create, :destroy] resources :triggers, only: [:index, :create, :destroy] diff --git a/db/migrate/20160608211215_add_user_default_external_to_application_settings.rb b/db/migrate/20160608211215_add_user_default_external_to_application_settings.rb new file mode 100644 index 00000000000..34c702e3fa6 --- /dev/null +++ b/db/migrate/20160608211215_add_user_default_external_to_application_settings.rb @@ -0,0 +1,13 @@ +class AddUserDefaultExternalToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + def up + add_column_with_default(:application_settings, :user_default_external, :boolean, + default: false, allow_null: false) + end + + def down + remove_column(:application_settings, :user_default_external) + end +end diff --git a/db/migrate/20160616102642_remove_duplicated_keys.rb b/db/migrate/20160616102642_remove_duplicated_keys.rb index 00a45d7fe73..180a75e0998 100644 --- a/db/migrate/20160616102642_remove_duplicated_keys.rb +++ b/db/migrate/20160616102642_remove_duplicated_keys.rb @@ -4,12 +4,12 @@ class RemoveDuplicatedKeys < ActiveRecord::Migration select_all("SELECT fingerprint FROM #{quote_table_name(:keys)} GROUP BY fingerprint HAVING COUNT(*) > 1").each do |row| fingerprint = connection.quote(row['fingerprint']) execute(%Q{ - DELETE FROM keys + DELETE FROM #{quote_table_name(:keys)} WHERE fingerprint = #{fingerprint} AND id != ( SELECT id FROM ( SELECT max(id) AS id - FROM keys + FROM #{quote_table_name(:keys)} WHERE fingerprint = #{fingerprint} ) max_ids ) diff --git a/db/migrate/20160620110927_fix_no_validatable_import_url.rb b/db/migrate/20160620110927_fix_no_validatable_import_url.rb deleted file mode 100644 index 82a616c62d9..00000000000 --- a/db/migrate/20160620110927_fix_no_validatable_import_url.rb +++ /dev/null @@ -1,86 +0,0 @@ -# Updates project records containing invalid URLs using the AddressableUrlValidator. -# This is optimized assuming the number of invalid records is low, but -# we still need to loop through all the projects with an +import_url+ -# so we use batching for the latter. -# -# This migration is non-reversible as we would have to keep the old data. - -class FixNoValidatableImportUrl < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - class SqlBatches - - attr_reader :results, :query - - def initialize(batch_size: 100, query:) - @offset = 0 - @batch_size = batch_size - @query = query - @results = [] - end - - def next? - @results = ActiveRecord::Base.connection.exec_query(batched_sql) - @offset += @batch_size - @results.any? - end - - private - - def batched_sql - "#{@query} LIMIT #{@batch_size} OFFSET #{@offset}" - end - end - - # AddressableValidator - Snapshot of AddressableUrlValidator - module AddressableUrlValidatorSnap - extend self - - def valid_url?(value) - return false unless value - - valid_uri?(value) && valid_protocol?(value) - rescue Addressable::URI::InvalidURIError - false - end - - def valid_uri?(value) - Addressable::URI.parse(value).is_a?(Addressable::URI) - end - - def valid_protocol?(value) - value =~ /\A#{URI.regexp(%w(http https ssh git))}\z/ - end - end - - def up - unless defined?(Addressable::URI::InvalidURIError) - say('Skipping cleaning up invalid import URLs as class from Addressable is missing') - return - end - - say('Cleaning up invalid import URLs... This may take a few minutes if we have a large number of imported projects.') - - invalid_import_url_project_ids.each { |project_id| cleanup_import_url(project_id) } - end - - def invalid_import_url_project_ids - ids = [] - batches = SqlBatches.new(query: "SELECT id, import_url FROM projects WHERE import_url IS NOT NULL") - - while batches.next? - batches.results.each do |result| - ids << result['id'] unless valid_url?(result['import_url']) - end - end - - ids - end - - def valid_url?(url) - AddressableUrlValidatorSnap.valid_url?(url) - end - - def cleanup_import_url(project_id) - execute("UPDATE projects SET import_url = NULL WHERE id = #{project_id}") - end -end diff --git a/db/schema.rb b/db/schema.rb index 68b9425253c..a5eea3a697c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -84,6 +84,7 @@ ActiveRecord::Schema.define(version: 20160705163108) do t.string "health_check_access_token" t.boolean "send_user_confirmation_email", default: false t.integer "container_registry_token_expire_delay", default: 5 + t.boolean "user_default_external", default: false, null: false t.text "after_sign_up_text" t.string "repository_storage", default: "default" t.string "enabled_git_access_protocol" diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md index 9fd7b71d2dc..e3306c22d3f 100644 --- a/doc/administration/custom_hooks.md +++ b/doc/administration/custom_hooks.md @@ -48,7 +48,8 @@ as appropriate. This feature was [introduced][5073] in GitLab 8.10. If the commit is declined or an error occurs during the Git hook check, -the STDERR and/or SDOUT message of the hook will be present in GitLab's UI. +the STDERR or STDOUT message of the hook will be present in GitLab's UI. +STDERR takes precedence over STDOUT. ![Custom message from custom Git hook](img/custom_hooks_error_msg.png) diff --git a/doc/api/groups.md b/doc/api/groups.md index 1ccb9715e96..87480bebfc4 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -42,46 +42,49 @@ Parameters: ```json
[
{
- "id": 4,
- "description": null,
+ "id": 9,
+ "description": "foo",
"default_branch": "master",
+ "tag_list": [],
"public": false,
- "visibility_level": 0,
- "ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",
- "http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",
- "web_url": "http://example.com/diaspora/diaspora-client",
- "tag_list": [
- "example",
- "disapora client"
- ],
- "owner": {
- "id": 3,
- "name": "Diaspora",
- "created_at": "2013-09-30T13: 46: 02Z"
- },
- "name": "Diaspora Client",
- "name_with_namespace": "Diaspora / Diaspora Client",
- "path": "diaspora-client",
- "path_with_namespace": "diaspora/diaspora-client",
+ "archived": false,
+ "visibility_level": 10,
+ "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git",
+ "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git",
+ "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate",
+ "name": "Html5 Boilerplate",
+ "name_with_namespace": "Experimental / Html5 Boilerplate",
+ "path": "html5-boilerplate",
+ "path_with_namespace": "h5bp/html5-boilerplate",
"issues_enabled": true,
"merge_requests_enabled": true,
- "builds_enabled": true,
"wiki_enabled": true,
- "snippets_enabled": false,
- "created_at": "2013-09-30T13: 46: 02Z",
- "last_activity_at": "2013-09-30T13: 46: 02Z",
- "creator_id": 3,
+ "builds_enabled": true,
+ "snippets_enabled": true,
+ "created_at": "2016-04-05T21:40:50.169Z",
+ "last_activity_at": "2016-04-06T16:52:08.432Z",
+ "shared_runners_enabled": true,
+ "creator_id": 1,
"namespace": {
- "created_at": "2013-09-30T13: 46: 02Z",
- "description": "",
- "id": 3,
- "name": "Diaspora",
- "owner_id": 1,
- "path": "diaspora",
- "updated_at": "2013-09-30T13: 46: 02Z"
+ "id": 5,
+ "name": "Experimental",
+ "path": "h5bp",
+ "owner_id": null,
+ "created_at": "2016-04-05T21:40:49.152Z",
+ "updated_at": "2016-04-07T08:07:48.466Z",
+ "description": "foo",
+ "avatar": {
+ "url": null
+ },
+ "share_with_group_lock": false,
+ "visibility_level": 10
},
- "archived": false,
- "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png"
+ "avatar_url": null,
+ "star_count": 1,
+ "forks_count": 0,
+ "open_issues_count": 3,
+ "public_builds": true,
+ "shared_with_groups": []
}
]
```
@@ -96,7 +99,180 @@ GET /groups/:id Parameters:
-- `id` (required) - The ID or path of a group
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or path of a group |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4
+```
+
+Example response:
+
+```json
+{
+ "id": 4,
+ "name": "Twitter",
+ "path": "twitter",
+ "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
+ "visibility_level": 20,
+ "avatar_url": null,
+ "web_url": "https://gitlab.example.com/groups/twitter",
+ "projects": [
+ {
+ "id": 7,
+ "description": "Voluptas veniam qui et beatae voluptas doloremque explicabo facilis.",
+ "default_branch": "master",
+ "tag_list": [],
+ "public": true,
+ "archived": false,
+ "visibility_level": 20,
+ "ssh_url_to_repo": "git@gitlab.example.com:twitter/typeahead-js.git",
+ "http_url_to_repo": "https://gitlab.example.com/twitter/typeahead-js.git",
+ "web_url": "https://gitlab.example.com/twitter/typeahead-js",
+ "name": "Typeahead.Js",
+ "name_with_namespace": "Twitter / Typeahead.Js",
+ "path": "typeahead-js",
+ "path_with_namespace": "twitter/typeahead-js",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "builds_enabled": true,
+ "snippets_enabled": false,
+ "container_registry_enabled": true,
+ "created_at": "2016-06-17T07:47:25.578Z",
+ "last_activity_at": "2016-06-17T07:47:25.881Z",
+ "shared_runners_enabled": true,
+ "creator_id": 1,
+ "namespace": {
+ "id": 4,
+ "name": "Twitter",
+ "path": "twitter",
+ "owner_id": null,
+ "created_at": "2016-06-17T07:47:24.216Z",
+ "updated_at": "2016-06-17T07:47:24.216Z",
+ "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
+ "avatar": {
+ "url": null
+ },
+ "share_with_group_lock": false,
+ "visibility_level": 20
+ },
+ "avatar_url": null,
+ "star_count": 0,
+ "forks_count": 0,
+ "open_issues_count": 3,
+ "public_builds": true,
+ "shared_with_groups": []
+ },
+ {
+ "id": 6,
+ "description": "Aspernatur omnis repudiandae qui voluptatibus eaque.",
+ "default_branch": "master",
+ "tag_list": [],
+ "public": false,
+ "archived": false,
+ "visibility_level": 10,
+ "ssh_url_to_repo": "git@gitlab.example.com:twitter/flight.git",
+ "http_url_to_repo": "https://gitlab.example.com/twitter/flight.git",
+ "web_url": "https://gitlab.example.com/twitter/flight",
+ "name": "Flight",
+ "name_with_namespace": "Twitter / Flight",
+ "path": "flight",
+ "path_with_namespace": "twitter/flight",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "builds_enabled": true,
+ "snippets_enabled": false,
+ "container_registry_enabled": true,
+ "created_at": "2016-06-17T07:47:24.661Z",
+ "last_activity_at": "2016-06-17T07:47:24.838Z",
+ "shared_runners_enabled": true,
+ "creator_id": 1,
+ "namespace": {
+ "id": 4,
+ "name": "Twitter",
+ "path": "twitter",
+ "owner_id": null,
+ "created_at": "2016-06-17T07:47:24.216Z",
+ "updated_at": "2016-06-17T07:47:24.216Z",
+ "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
+ "avatar": {
+ "url": null
+ },
+ "share_with_group_lock": false,
+ "visibility_level": 20
+ },
+ "avatar_url": null,
+ "star_count": 0,
+ "forks_count": 0,
+ "open_issues_count": 8,
+ "public_builds": true,
+ "shared_with_groups": []
+ }
+ ],
+ "shared_projects": [
+ {
+ "id": 8,
+ "description": "Velit eveniet provident fugiat saepe eligendi autem.",
+ "default_branch": "master",
+ "tag_list": [],
+ "public": false,
+ "archived": false,
+ "visibility_level": 0,
+ "ssh_url_to_repo": "git@gitlab.example.com:h5bp/html5-boilerplate.git",
+ "http_url_to_repo": "https://gitlab.example.com/h5bp/html5-boilerplate.git",
+ "web_url": "https://gitlab.example.com/h5bp/html5-boilerplate",
+ "name": "Html5 Boilerplate",
+ "name_with_namespace": "H5bp / Html5 Boilerplate",
+ "path": "html5-boilerplate",
+ "path_with_namespace": "h5bp/html5-boilerplate",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "builds_enabled": true,
+ "snippets_enabled": false,
+ "container_registry_enabled": true,
+ "created_at": "2016-06-17T07:47:27.089Z",
+ "last_activity_at": "2016-06-17T07:47:27.310Z",
+ "shared_runners_enabled": true,
+ "creator_id": 1,
+ "namespace": {
+ "id": 5,
+ "name": "H5bp",
+ "path": "h5bp",
+ "owner_id": null,
+ "created_at": "2016-06-17T07:47:26.621Z",
+ "updated_at": "2016-06-17T07:47:26.621Z",
+ "description": "Id consequatur rem vel qui doloremque saepe.",
+ "avatar": {
+ "url": null
+ },
+ "share_with_group_lock": false,
+ "visibility_level": 20
+ },
+ "avatar_url": null,
+ "star_count": 0,
+ "forks_count": 0,
+ "open_issues_count": 4,
+ "public_builds": true,
+ "shared_with_groups": [
+ {
+ "group_id": 4,
+ "group_name": "Twitter",
+ "group_access_level": 30
+ },
+ {
+ "group_id": 3,
+ "group_name": "Gitlab Org",
+ "group_access_level": 10
+ }
+ ]
+ }
+ ]
+}
+```
## New group
@@ -201,7 +377,8 @@ Example response: "star_count": 1,
"forks_count": 0,
"open_issues_count": 3,
- "public_builds": true
+ "public_builds": true,
+ "shared_with_groups": []
}
]
}
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index aee94b3fc36..816f09e1007 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -49,10 +49,10 @@ Parameters: "state": "active", "created_at": "2012-04-29T08:46:00Z" }, - "source_project_id": "2", - "target_project_id": "3", + "source_project_id": 2, + "target_project_id": 3, "labels": [ ], - "description":"fixed login page css paddings", + "description": "fixed login page css paddings", "work_in_progress": false, "milestone": { "id": 5, @@ -113,10 +113,10 @@ Parameters: "state": "active", "created_at": "2012-04-29T08:46:00Z" }, - "source_project_id": "2", - "target_project_id": "3", + "source_project_id": 2, + "target_project_id": 3, "labels": [ ], - "description":"fixed login page css paddings", + "description": "fixed login page css paddings", "work_in_progress": false, "milestone": { "id": 5, @@ -296,7 +296,7 @@ Parameters: "source_project_id": 4, "target_project_id": 4, "labels": [ ], - "description":"fixed login page css paddings", + "description": "fixed login page css paddings", "work_in_progress": false, "milestone": { "id": 5, @@ -465,7 +465,7 @@ Parameters: "source_project_id": 4, "target_project_id": 4, "labels": [ ], - "description":"fixed login page css paddings", + "description": "fixed login page css paddings", "work_in_progress": false, "milestone": { "id": 5, @@ -531,7 +531,7 @@ Parameters: "source_project_id": 4, "target_project_id": 4, "labels": [ ], - "description":"fixed login page css paddings", + "description": "fixed login page css paddings", "work_in_progress": false, "milestone": { "id": 5, diff --git a/doc/api/projects.md b/doc/api/projects.md index f5f195b97df..dceee7b4ea7 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -52,7 +52,7 @@ Parameters: "owner": { "id": 3, "name": "Diaspora", - "created_at": "2013-09-30T13: 46: 02Z" + "created_at": "2013-09-30T13:46:02Z" }, "name": "Diaspora Client", "name_with_namespace": "Diaspora / Diaspora Client", @@ -64,17 +64,18 @@ Parameters: "builds_enabled": true, "wiki_enabled": true, "snippets_enabled": false, - "created_at": "2013-09-30T13: 46: 02Z", - "last_activity_at": "2013-09-30T13: 46: 02Z", + "container_registry_enabled": false, + "created_at": "2013-09-30T13:46:02Z", + "last_activity_at": "2013-09-30T13:46:02Z", "creator_id": 3, "namespace": { - "created_at": "2013-09-30T13: 46: 02Z", + "created_at": "2013-09-30T13:46:02Z", "description": "", "id": 3, "name": "Diaspora", "owner_id": 1, "path": "diaspora", - "updated_at": "2013-09-30T13: 46: 02Z" + "updated_at": "2013-09-30T13:46:02Z" }, "archived": false, "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png", @@ -82,7 +83,8 @@ Parameters: "forks_count": 0, "star_count": 0, "runners_token": "b8547b1dc37721d05889db52fa2f02", - "public_builds": true + "public_builds": true, + "shared_with_groups": [] }, { "id": 6, @@ -112,6 +114,7 @@ Parameters: "builds_enabled": true, "wiki_enabled": true, "snippets_enabled": false, + "container_registry_enabled": false, "created_at": "2013-09-30T13:46:02Z", "last_activity_at": "2013-09-30T13:46:02Z", "creator_id": 3, @@ -140,7 +143,8 @@ Parameters: "forks_count": 0, "star_count": 0, "runners_token": "b8547b1dc37721d05889db52fa2f02", - "public_builds": true + "public_builds": true, + "shared_with_groups": [] } ] ``` @@ -223,7 +227,7 @@ Parameters: "owner": { "id": 3, "name": "Diaspora", - "created_at": "2013-09-30T13: 46: 02Z" + "created_at": "2013-09-30T13:46:02Z" }, "name": "Diaspora Project Site", "name_with_namespace": "Diaspora / Diaspora Project Site", @@ -235,17 +239,18 @@ Parameters: "builds_enabled": true, "wiki_enabled": true, "snippets_enabled": false, - "created_at": "2013-09-30T13: 46: 02Z", - "last_activity_at": "2013-09-30T13: 46: 02Z", + "container_registry_enabled": false, + "created_at": "2013-09-30T13:46:02Z", + "last_activity_at": "2013-09-30T13:46:02Z", "creator_id": 3, "namespace": { - "created_at": "2013-09-30T13: 46: 02Z", + "created_at": "2013-09-30T13:46:02Z", "description": "", "id": 3, "name": "Diaspora", "owner_id": 1, "path": "diaspora", - "updated_at": "2013-09-30T13: 46: 02Z" + "updated_at": "2013-09-30T13:46:02Z" }, "permissions": { "project_access": { @@ -262,7 +267,20 @@ Parameters: "shared_runners_enabled": true, "forks_count": 0, "star_count": 0, - "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b" + "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b", + "public_builds": true, + "shared_with_groups": [ + { + "group_id": 4, + "group_name": "Twitter", + "group_access_level": 30 + }, + { + "group_id": 3, + "group_name": "Gitlab Org", + "group_access_level": 10 + } + ] } ``` @@ -425,6 +443,7 @@ Parameters: - `wiki_enabled` (optional) - `snippets_enabled` (optional) - `container_registry_enabled` (optional) +- `shared_runners_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `import_url` (optional) @@ -449,6 +468,7 @@ Parameters: - `wiki_enabled` (optional) - `snippets_enabled` (optional) - `container_registry_enabled` (optional) +- `shared_runners_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `import_url` (optional) @@ -475,6 +495,7 @@ Parameters: - `wiki_enabled` (optional) - `snippets_enabled` (optional) - `container_registry_enabled` (optional) +- `shared_runners_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `public_builds` (optional) @@ -537,23 +558,26 @@ Example response: "builds_enabled": true, "wiki_enabled": true, "snippets_enabled": false, - "created_at": "2013-09-30T13: 46: 02Z", - "last_activity_at": "2013-09-30T13: 46: 02Z", + "container_registry_enabled": false, + "created_at": "2013-09-30T13:46:02Z", + "last_activity_at": "2013-09-30T13:46:02Z", "creator_id": 3, "namespace": { - "created_at": "2013-09-30T13: 46: 02Z", + "created_at": "2013-09-30T13:46:02Z", "description": "", "id": 3, "name": "Diaspora", "owner_id": 1, "path": "diaspora", - "updated_at": "2013-09-30T13: 46: 02Z" + "updated_at": "2013-09-30T13:46:02Z" }, "archived": true, "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", "shared_runners_enabled": true, "forks_count": 0, - "star_count": 1 + "star_count": 1, + "public_builds": true, + "shared_with_groups": [] } ``` @@ -600,23 +624,26 @@ Example response: "builds_enabled": true, "wiki_enabled": true, "snippets_enabled": false, - "created_at": "2013-09-30T13: 46: 02Z", - "last_activity_at": "2013-09-30T13: 46: 02Z", + "container_registry_enabled": false, + "created_at": "2013-09-30T13:46:02Z", + "last_activity_at": "2013-09-30T13:46:02Z", "creator_id": 3, "namespace": { - "created_at": "2013-09-30T13: 46: 02Z", + "created_at": "2013-09-30T13:46:02Z", "description": "", "id": 3, "name": "Diaspora", "owner_id": 1, "path": "diaspora", - "updated_at": "2013-09-30T13: 46: 02Z" + "updated_at": "2013-09-30T13:46:02Z" }, "archived": true, "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", "shared_runners_enabled": true, "forks_count": 0, - "star_count": 0 + "star_count": 0, + "public_builds": true, + "shared_with_groups": [] } ``` @@ -660,7 +687,7 @@ Example response: "owner": { "id": 3, "name": "Diaspora", - "created_at": "2013-09-30T13: 46: 02Z" + "created_at": "2013-09-30T13:46:02Z" }, "name": "Diaspora Project Site", "name_with_namespace": "Diaspora / Diaspora Project Site", @@ -672,17 +699,18 @@ Example response: "builds_enabled": true, "wiki_enabled": true, "snippets_enabled": false, - "created_at": "2013-09-30T13: 46: 02Z", - "last_activity_at": "2013-09-30T13: 46: 02Z", + "container_registry_enabled": false, + "created_at": "2013-09-30T13:46:02Z", + "last_activity_at": "2013-09-30T13:46:02Z", "creator_id": 3, "namespace": { - "created_at": "2013-09-30T13: 46: 02Z", + "created_at": "2013-09-30T13:46:02Z", "description": "", "id": 3, "name": "Diaspora", "owner_id": 1, "path": "diaspora", - "updated_at": "2013-09-30T13: 46: 02Z" + "updated_at": "2013-09-30T13:46:02Z" }, "permissions": { "project_access": { @@ -699,7 +727,9 @@ Example response: "shared_runners_enabled": true, "forks_count": 0, "star_count": 0, - "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b" + "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b", + "public_builds": true, + "shared_with_groups": [] } ``` @@ -713,7 +743,7 @@ have the proper access rights, code 403 is returned. Status 404 is returned if t doesn't exist, or is hidden to the user. ``` -POST /projects/:id/archive +POST /projects/:id/unarchive ``` | Attribute | Type | Required | Description | @@ -743,7 +773,7 @@ Example response: "owner": { "id": 3, "name": "Diaspora", - "created_at": "2013-09-30T13: 46: 02Z" + "created_at": "2013-09-30T13:46:02Z" }, "name": "Diaspora Project Site", "name_with_namespace": "Diaspora / Diaspora Project Site", @@ -755,17 +785,18 @@ Example response: "builds_enabled": true, "wiki_enabled": true, "snippets_enabled": false, - "created_at": "2013-09-30T13: 46: 02Z", - "last_activity_at": "2013-09-30T13: 46: 02Z", + "container_registry_enabled": false, + "created_at": "2013-09-30T13:46:02Z", + "last_activity_at": "2013-09-30T13:46:02Z", "creator_id": 3, "namespace": { - "created_at": "2013-09-30T13: 46: 02Z", + "created_at": "2013-09-30T13:46:02Z", "description": "", "id": 3, "name": "Diaspora", "owner_id": 1, "path": "diaspora", - "updated_at": "2013-09-30T13: 46: 02Z" + "updated_at": "2013-09-30T13:46:02Z" }, "permissions": { "project_access": { @@ -782,7 +813,9 @@ Example response: "shared_runners_enabled": true, "forks_count": 0, "star_count": 0, - "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b" + "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b", + "public_builds": true, + "shared_with_groups": [] } ``` @@ -965,11 +998,11 @@ Parameters: "id": 1, "url": "http://example.com/hook", "project_id": 3, - "push_events": "true", - "issues_events": "true", - "merge_requests_events": "true", - "note_events": "true", - "enable_ssl_verification": "true", + "push_events": true, + "issues_events": true, + "merge_requests_events": true, + "note_events": true, + "enable_ssl_verification": true, "created_at": "2012-10-12T17:04:47Z" } ``` @@ -1089,8 +1122,8 @@ Parameters: "name": "Jeremy Ashkenas", "email": "jashkenas@example.com" }, - "authored_date": "2013-09-07T12: 58: 21+00: 00", - "committed_date": "2013-09-07T12: 58: 21+00: 00" + "authored_date": "2013-09-07T12:58:21+00:00", + "committed_date": "2013-09-07T12:58:21+00:00" }, "protected": false } diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md index 17e1c64bb8a..bfafcc44d66 100644 --- a/doc/ci/examples/php.md +++ b/doc/ci/examples/php.md @@ -49,7 +49,7 @@ apt-get update -yqq apt-get install git -yqq # Install phpunit, the tool that we will use for testing -curl -o /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar +curl -Lo /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar chmod +x /usr/local/bin/phpunit # Install mysql driver diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index d2d1b04f893..16a1461a7e4 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -133,7 +133,7 @@ builds, including deploy builds. This can be an array or a multi-line string. ### after_script >**Note:** -Introduced in GitLab 8.7 and requires Gitlab Runner v1.2 (not yet released) +Introduced in GitLab 8.7 and requires Gitlab Runner v1.2 `after_script` is used to define the command that will be run after for all builds. This has to be an array or a multi-line string. @@ -811,7 +811,7 @@ deploy: It's possible to overwrite globally defined `before_script` and `after_script`: ```yaml -before_script +before_script: - global before script job: diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md index 5893b7c219e..ce0aaa2fd25 100644 --- a/doc/development/ui_guide.md +++ b/doc/development/ui_guide.md @@ -52,5 +52,6 @@ information from database or file system * Use red button for destructive actions (not revertable). For example removing issue. * Use green or blue button for primary action. Primary button should be only one. Do not use both green and blue button in one form. -* For all other cases use default white button +* For all other cases use default white button. +* Text button should have only first word capitalized. So should be "Create issue" instead of "Create Issue" diff --git a/doc/integration/github.md b/doc/integration/github.md index e7497e475c9..340c8a55fb3 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -19,7 +19,7 @@ GitHub will generate an application ID and secret key for you to use. - Application name: This can be anything. Consider something like "\<Organization\>'s GitLab" or "\<Your Name\>'s GitLab" or something else descriptive. - Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com' - Application description: Fill this in if you wish. - - Default authorization callback URL is '${YOUR_DOMAIN}/import/github/callback' + - Authorization callback URL is 'http(s)://${YOUR_DOMAIN}' 1. Select "Register application". 1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index 236eb7b12c4..fb2dd582754 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -7,11 +7,12 @@ * [Newlines](#newlines) * [Multiple underscores in words](#multiple-underscores-in-words) * [URL auto-linking](#url-auto-linking) +* [Multiline Blockquote](#multiline-blockquote) * [Code and Syntax Highlighting](#code-and-syntax-highlighting) * [Inline Diff](#inline-diff) * [Emoji](#emoji) * [Special GitLab references](#special-gitlab-references) -* [Task lists](#task-lists) +* [Task Lists](#task-lists) **[Standard Markdown](#standard-markdown)** @@ -89,6 +90,37 @@ GFM will autolink almost any URL you copy and paste into your text. * irc://irc.freenode.net/gitlab * http://localhost:3000 +## Multiline Blockquote + +On top of standard Markdown [blockquotes](#blockquotes), which require prepending `>` to quoted lines, +GFM supports multiline blockquotes fenced by <code>>>></code>. + +```no-highlight +>>> +If you paste a message from somewhere else + +that + +spans + +multiple lines, + +you can quote that without having to manually prepend `>` to every line! +>>> +``` + +>>> +If you paste a message from somewhere else + +that + +spans + +multiple lines, + +you can quote that without having to manually prepend `>` to every line! +>>> + ## Code and Syntax Highlighting _GitLab uses the [Rouge Ruby library][rouge] for syntax highlighting. For a diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 963b35de3a0..44f3f6d3b12 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -99,3 +99,6 @@ An administrator can flag a user as external [through the API](../api/users.md) or by checking the checkbox on the admin panel. As an administrator, navigate to **Admin > Users** to create a new user or edit an existing one. There, you will find the option to flag the user as external. + +By default new users are not set as external users. This behavior can be changed +by an administrator under **Admin > Application Settings**.
\ No newline at end of file diff --git a/doc/update/8.9-to-8.10.md b/doc/update/8.9-to-8.10.md index a51790b0bda..84065a84e50 100644 --- a/doc/update/8.9-to-8.10.md +++ b/doc/update/8.9-to-8.10.md @@ -46,7 +46,7 @@ sudo -u git -H git checkout 8-10-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v3.1.0 +sudo -u git -H git checkout v3.2.0 ``` ### 5. Update gitlab-workhorse diff --git a/doc/workflow/protected_branches.md b/doc/workflow/protected_branches.md index d854ec1e025..67adfc2f43a 100644 --- a/doc/workflow/protected_branches.md +++ b/doc/workflow/protected_branches.md @@ -1,4 +1,4 @@ -# Protected branches +# Protected Branches Permissions in GitLab are fundamentally defined around the idea of having read or write permission to the repository and branches. @@ -28,4 +28,28 @@ For those workflows, you can allow everyone with write access to push to a prote On already protected branches you can also allow developers to push to the repository by selecting the `Developers can push` check box. -![Developers can push](protected_branches/protected_branches2.png)
\ No newline at end of file +![Developers can push](protected_branches/protected_branches2.png) + +## Wildcard Protected Branches + +>**Note:** +This feature was added in GitLab 8.10. + +1. You can specify a wildcard protected branch, which will protect all branches matching the wildcard. For example: + + | Wildcard Protected Branch | Matching Branches | + |---------------------------+--------------------------------------------------------| + | `*-stable` | `production-stable`, `staging-stable` | + | `production/*` | `production/app-server`, `production/load-balancer` | + | `*gitlab*` | `gitlab`, `gitlab/staging`, `master/gitlab/production` | + +1. Protected branch settings (like "Developers Can Push") apply to all matching branches. + +1. Two different wildcards can potentially match the same branch. For example, `*-stable` and `production-*` would both match a `production-stable` branch. + >**Note:** + If _any_ of these protected branches have "Developers Can Push" set to true, then `production-stable` has it set to true. + +1. If you click on a protected branch's name, you will be presented with a list of all matching branches: + + ![protected branch matches](protected_branches/protected_branches3.png) + diff --git a/doc/workflow/protected_branches/protected_branches1.png b/doc/workflow/protected_branches/protected_branches1.png Binary files differindex bb3ab7d7913..c00443803de 100644 --- a/doc/workflow/protected_branches/protected_branches1.png +++ b/doc/workflow/protected_branches/protected_branches1.png diff --git a/doc/workflow/protected_branches/protected_branches2.png b/doc/workflow/protected_branches/protected_branches2.png Binary files differindex 58ace31ac57..a4f664d3b21 100644 --- a/doc/workflow/protected_branches/protected_branches2.png +++ b/doc/workflow/protected_branches/protected_branches2.png diff --git a/doc/workflow/protected_branches/protected_branches3.png b/doc/workflow/protected_branches/protected_branches3.png Binary files differnew file mode 100644 index 00000000000..2a50cb174bb --- /dev/null +++ b/doc/workflow/protected_branches/protected_branches3.png diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index db73309804c..1f4c9020731 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -7,6 +7,7 @@ Feature: Dashboard And project "Shop" has CI enabled And project "Shop" has CI build And project "Shop" has labels: "bug", "feature", "enhancement" + And project "Shop" has issue: "bug report" And I visit dashboard page Scenario: I should see projects list diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature index a95df038357..8b0cb90765e 100644 --- a/features/project/commits/commits.feature +++ b/features/project/commits/commits.feature @@ -83,11 +83,6 @@ Feature: Project Commits #Given I visit my project's commits stats page #Then I see commits stats - Scenario: I browse big commit - Given I visit big commit page - Then I see big commit warning - And I see "Reload with full diff" link - Scenario: I browse a commit with an image Given I visit a commit with an image that changed Then The diff links to both the previous and current image diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 0e97e4d5954..21768c15c17 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -89,13 +89,6 @@ Feature: Project Merge Requests Then The list should be sorted by "Oldest updated" @javascript - Scenario: Visiting Issues after being sorted the list - Given I visit project "Shop" merge requests page - And I sort the list by "Oldest updated" - And I visit project "Shop" issues page - Then The list should be sorted by "Oldest updated" - - @javascript Scenario: Visiting Merge Requests from a differente Project after sorting Given I visit project "Shop" merge requests page And I sort the list by "Oldest updated" diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb index 2876e8812e9..b4a32ed2e38 100644 --- a/features/steps/project/builds/artifacts.rb +++ b/features/steps/project/builds/artifacts.rb @@ -68,10 +68,16 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps end step 'download of a file extracted from build artifacts should start' do - # this will be accelerated by Workhorse - response_json = JSON.parse(page.body, symbolize_names: true) - expect(response_json[:archive]).to end_with('build_artifacts.zip') - expect(response_json[:entry]).to eq Base64.encode64('ci_artifacts.txt') + send_data = response_headers[Gitlab::Workhorse::SEND_DATA_HEADER] + + expect(send_data).to start_with('artifacts-entry:') + + base64_params = send_data.sub(/\Aartifacts\-entry:/, '') + params = JSON.parse(Base64.urlsafe_decode64(base64_params)) + + expect(params.keys).to eq(['Archive', 'Entry']) + expect(params['Archive']).to end_with('build_artifacts.zip') + expect(params['Entry']).to eq(Base64.encode64('ci_artifacts.txt')) end step 'I click a first row within build artifacts table' do diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 239036e431d..bea9f9d198b 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -125,25 +125,6 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps expect(page).to have_content 'Authors' end - step 'I visit big commit page' do - # Create a temporary scope to ensure that the stub_const is removed after user - RSpec::Mocks.with_temporary_scope do - stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_lines: 1, max_files: 1 }) - visit namespace_project_commit_path(@project.namespace, @project, sample_big_commit.id) - end - end - - step 'I see big commit warning' do - expect(page).to have_content sample_big_commit.message - expect(page).to have_content "Too many changes" - end - - step 'I see "Reload with full diff" link' do - link = find_link('Reload with full diff') - expect(link[:href]).to end_with('?force_show_diff=true') - expect(link[:href]).not_to include('.html') - end - step 'I visit a commit with an image that changed' do visit namespace_project_commit_path(@project.namespace, @project, sample_image_commit.id) end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index b3411c03118..0b4920883b8 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -223,6 +223,11 @@ module SharedProject create(:label, project: project, title: 'enhancement') end + step 'project "Shop" has issue: "bug report"' do + project = Project.find_by(name: "Shop") + create(:issue, project: project, title: "bug report") + end + step 'project "Shop" has CI enabled' do project = Project.find_by(name: "Shop") project.enable_ci diff --git a/lib/api/entities.rb b/lib/api/entities.rb index db877d2eeb0..8edb80177da 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -58,6 +58,14 @@ module API expose :path, :path_with_namespace end + class SharedGroup < Grape::Entity + expose :group_id + expose :group_name do |group_link, options| + group_link.group.name + end + expose :group_access, as: :group_access_level + end + class Project < Grape::Entity expose :id, :description, :default_branch, :tag_list expose :public?, as: :public @@ -77,6 +85,9 @@ module API expose :open_issues_count, if: lambda { |project, options| project.issues_enabled? && project.default_issues_tracker? } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :public_builds + expose :shared_with_groups do |project, options| + SharedGroup.represent(project.project_group_links.all, options) + end end class ProjectMember < UserBasic @@ -93,6 +104,7 @@ module API class GroupDetail < Group expose :projects, using: Entities::Project + expose :shared_projects, using: Entities::Project end class GroupMember < UserBasic @@ -187,7 +199,6 @@ module API expose :author, :assignee, using: Entities::UserBasic expose :source_project_id, :target_project_id expose :label_names, as: :labels - expose :description expose :work_in_progress?, as: :work_in_progress expose :milestone, using: Entities::Milestone expose :merge_when_build_succeeds diff --git a/lib/banzai/filter/blockquote_fence_filter.rb b/lib/banzai/filter/blockquote_fence_filter.rb new file mode 100644 index 00000000000..d2c4b1e4d76 --- /dev/null +++ b/lib/banzai/filter/blockquote_fence_filter.rb @@ -0,0 +1,71 @@ +module Banzai + module Filter + class BlockquoteFenceFilter < HTML::Pipeline::TextFilter + REGEX = %r{ + (?<code> + # Code blocks: + # ``` + # Anything, including `>>>` blocks which are ignored by this filter + # ``` + + ^``` + .+? + \n```$ + ) + | + (?<html> + # HTML block: + # <tag> + # Anything, including `>>>` blocks which are ignored by this filter + # </tag> + + ^<[^>]+?>\n + .+? + \n<\/[^>]+?>$ + ) + | + (?: + # Blockquote: + # >>> + # Anything, including code and HTML blocks + # >>> + + ^>>>\n + (?<quote> + (?: + # Any character that doesn't introduce a code or HTML block + (?! + ^``` + | + ^<[^>]+?>\n + ) + . + | + # A code block + \g<code> + | + # An HTML block + \g<html> + )+? + ) + \n>>>$ + ) + }mx.freeze + + def initialize(text, context = nil, result = nil) + super text, context, result + @text = @text.delete("\r") + end + + def call + @text.gsub(REGEX) do + if $~[:quote] + $~[:quote].gsub(/^/, "> ").gsub(/^> $/, ">") + else + $~[0] + end + end + end + end + end +end diff --git a/lib/banzai/pipeline/pre_process_pipeline.rb b/lib/banzai/pipeline/pre_process_pipeline.rb index 50dc978b452..6cf219661d3 100644 --- a/lib/banzai/pipeline/pre_process_pipeline.rb +++ b/lib/banzai/pipeline/pre_process_pipeline.rb @@ -3,7 +3,8 @@ module Banzai class PreProcessPipeline < BasePipeline def self.filters FilterArray[ - Filter::YamlFrontMatterFilter + Filter::YamlFrontMatterFilter, + Filter::BlockquoteFenceFilter, ] end diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb index 3d7b9c4a024..6cf218aaa0d 100644 --- a/lib/banzai/reference_parser/base_parser.rb +++ b/lib/banzai/reference_parser/base_parser.rb @@ -133,8 +133,9 @@ module Banzai return {} if nodes.empty? ids = unique_attribute_values(nodes, attribute) + rows = collection_objects_for_ids(collection, ids) - collection.where(id: ids).each_with_object({}) do |row, hash| + rows.each_with_object({}) do |row, hash| hash[row.id] = row end end @@ -153,6 +154,31 @@ module Banzai values.to_a end + # Queries the collection for the objects with the given IDs. + # + # If the RequestStore module is enabled this method will only query any + # objects that have not yet been queried. For objects that have already + # been queried the object is returned from the cache. + def collection_objects_for_ids(collection, ids) + if RequestStore.active? + cache = collection_cache[collection_cache_key(collection)] + to_query = ids.map(&:to_i) - cache.keys + + unless to_query.empty? + collection.where(id: to_query).each { |row| cache[row.id] = row } + end + + cache.values + else + collection.where(id: ids) + end + end + + # Returns the cache key to use for a collection. + def collection_cache_key(collection) + collection.respond_to?(:model) ? collection.model : collection + end + # Processes the list of HTML documents and returns an Array containing all # the references. def process(documents) @@ -189,7 +215,7 @@ module Banzai end def find_projects_for_hash_keys(hash) - Project.where(id: hash.keys) + collection_objects_for_ids(Project, hash.keys) end private @@ -199,6 +225,12 @@ module Banzai def lazy(&block) Gitlab::Lazy.new(&block) end + + def collection_cache + RequestStore[:banzai_collection_cache] ||= Hash.new do |hash, key| + hash[key] = {} + end + end end end end diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb index a12b0d19560..863f5725d3b 100644 --- a/lib/banzai/reference_parser/user_parser.rb +++ b/lib/banzai/reference_parser/user_parser.rb @@ -73,7 +73,7 @@ module Banzai def find_users(ids) return [] if ids.empty? - User.where(id: ids).to_a + collection_objects_for_ids(User, ids) end def find_users_for_groups(ids) @@ -85,7 +85,8 @@ module Banzai def find_users_for_projects(ids) return [] if ids.empty? - Project.where(id: ids).flat_map { |p| p.team.members.to_a } + collection_objects_for_ids(Project, ids). + flat_map { |p| p.team.members.to_a } end end end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 54b46e5d23f..ffc1814b29d 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -48,6 +48,7 @@ module Gitlab akismet_enabled: false, repository_checks_enabled: true, container_registry_token_expire_delay: 5, + user_default_external: false, ) end diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index b0c50edba59..7e01f7b61fb 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -68,6 +68,10 @@ module Gitlab @lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a end + def collapsed_by_default? + diff.diff.bytesize > 10240 # 10 KB + end + def highlighted_diff_lines @highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight end diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb index 57c41b41298..9b681e636c7 100644 --- a/lib/gitlab/git/hook.rb +++ b/lib/gitlab/git/hook.rb @@ -15,7 +15,7 @@ module Gitlab end def trigger(gl_id, oldrev, newrev, ref) - return true unless exists? + return [true, nil] unless exists? case name when "pre-receive", "post-receive" @@ -70,13 +70,10 @@ module Gitlab end def call_update_hook(gl_id, oldrev, newrev, ref) - status = nil - Dir.chdir(repo_path) do - status = system({ 'GL_ID' => gl_id }, path, ref, oldrev, newrev) + stdout, stderr, status = Open3.capture3({ 'GL_ID' => gl_id }, path, ref, oldrev, newrev) + [status.success?, stderr.presence || stdout] end - - [status, nil] end def retrieve_error_message(stderr, stdout) diff --git a/lib/gitlab/graphs/commits.rb b/lib/gitlab/graphs/commits.rb index 2122339d2db..3caf9036459 100644 --- a/lib/gitlab/graphs/commits.rb +++ b/lib/gitlab/graphs/commits.rb @@ -18,7 +18,7 @@ module Gitlab end def commit_per_day - @commit_per_day ||= (@commits.size.to_f / @duration).round(1) + @commit_per_day ||= @commits.size / (@duration + 1) end def collect_data diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 0ac6ff01e3b..025ecc12f9f 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -69,10 +69,19 @@ module Gitlab # Example: # +relation_key+ issues, loops through the list of *issues* and for each individual # issue, finds any subrelations such as notes, creates them and assign them back to the hash + # + # Recursively calls this method if the sub-relation is a hash containing more sub-relations def create_sub_relations(relation, tree_hash) relation_key = relation.keys.first.to_s + return if tree_hash[relation_key].blank? + tree_hash[relation_key].each do |relation_item| relation.values.flatten.each do |sub_relation| + # We just use author to get the user ID, do not attempt to create an instance. + next if sub_relation == :author + + create_sub_relations(sub_relation, relation_item) if sub_relation.is_a?(Hash) + relation_hash, sub_relation = assign_relation_hash(relation_item, sub_relation) relation_item[sub_relation.to_s] = create_relation(sub_relation, relation_hash) unless relation_hash.blank? end diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb index 86ed18fb50d..19dad699edf 100644 --- a/lib/gitlab/url_sanitizer.rb +++ b/lib/gitlab/url_sanitizer.rb @@ -4,6 +4,8 @@ module Gitlab regexp = URI::Parser.new.make_regexp(['http', 'https', 'ssh', 'git']) content.gsub(regexp) { |url| new(url).masked_url } + rescue Addressable::URI::InvalidURIError + content.gsub(regexp, '') end def self.valid?(url) diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index bc0193a6c32..6aeb49c0219 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -63,6 +63,18 @@ module Gitlab ] end + def send_artifacts_entry(build, entry) + params = { + 'Archive' => build.artifacts_file.path, + 'Entry' => Base64.encode64(entry.path) + } + + [ + SEND_DATA_HEADER, + "artifacts-entry:#{encode(params)}" + ] + end + protected def encode(hash) diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb index 8c309efc7b8..3358ed6773e 100644 --- a/lib/rouge/formatters/html_gitlab.rb +++ b/lib/rouge/formatters/html_gitlab.rb @@ -143,18 +143,14 @@ module Rouge '</span>' end end - lines.join("\n") - else - if @linenos == 'inline' - lines = lines.each_with_index.map do |line, index| - number = index + @linenostart - "<span class=\"linenos\">#{number}</span>#{line}" - end - lines.join("\n") - else - lines.join("\n") + elsif @linenos == 'inline' + lines = lines.each_with_index.map do |line, index| + number = index + @linenostart + "<span class=\"linenos\">#{number}</span>#{line}" end end + + lines.join("\n") end def span(tok, val) diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb deleted file mode 100644 index a3a3309e15e..00000000000 --- a/spec/controllers/commit_controller_spec.rb +++ /dev/null @@ -1,246 +0,0 @@ -require 'spec_helper' - -describe Projects::CommitController do - let(:project) { create(:project) } - let(:user) { create(:user) } - let(:commit) { project.commit("master") } - let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' } - let(:master_pickable_commit) { project.commit(master_pickable_sha) } - - before do - sign_in(user) - project.team << [user, :master] - end - - describe "#show" do - shared_examples "export as" do |format| - it "should generally work" do - get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: commit.id, - format: format) - - expect(response).to be_success - end - - it "should generate it" do - expect_any_instance_of(Commit).to receive(:"to_#{format}") - - get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: commit.id, format: format) - end - - it "should render it" do - get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: commit.id, format: format) - - expect(response.body).to eq(commit.send(:"to_#{format}")) - end - - it "should not escape Html" do - allow_any_instance_of(Commit).to receive(:"to_#{format}"). - and_return('HTML entities &<>" ') - - get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: commit.id, format: format) - - expect(response.body).not_to include('&') - expect(response.body).not_to include('>') - expect(response.body).not_to include('<') - expect(response.body).not_to include('"') - end - end - - describe "as diff" do - include_examples "export as", :diff - let(:format) { :diff } - - it "should really only be a git diff" do - get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: commit.id, - format: format) - - expect(response.body).to start_with("diff --git") - end - - it "should really only be a git diff without whitespace changes" do - get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: '66eceea0db202bb39c4e445e8ca28689645366c5', - # id: commit.id, - format: format, - w: 1) - - expect(response.body).to start_with("diff --git") - # without whitespace option, there are more than 2 diff_splits - diff_splits = assigns(:diffs).first.diff.split("\n") - expect(diff_splits.length).to be <= 2 - end - end - - describe "as patch" do - include_examples "export as", :patch - let(:format) { :patch } - - it "should really be a git email patch" do - get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: commit.id, - format: format) - - expect(response.body).to start_with("From #{commit.id}") - end - - it "should contain a git diff" do - get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: commit.id, - format: format) - - expect(response.body).to match(/^diff --git/) - end - end - - context 'commit that removes a submodule' do - render_views - - let(:fork_project) { create(:forked_project_with_submodules) } - let(:commit) { fork_project.commit('remove-submodule') } - - before do - fork_project.team << [user, :master] - end - - it 'renders it' do - get(:show, - namespace_id: fork_project.namespace.to_param, - project_id: fork_project.to_param, - id: commit.id) - - expect(response).to be_success - end - end - end - - describe "#branches" do - it "contains branch and tags information" do - get(:branches, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: commit.id) - - expect(assigns(:branches)).to include("master", "feature_conflict") - expect(assigns(:tags)).to include("v1.1.0") - end - end - - describe '#revert' do - context 'when target branch is not provided' do - it 'should render the 404 page' do - post(:revert, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: commit.id) - - expect(response).not_to be_success - expect(response).to have_http_status(404) - end - end - - context 'when the revert was successful' do - it 'should redirect to the commits page' do - post(:revert, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - target_branch: 'master', - id: commit.id) - - expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master') - expect(flash[:notice]).to eq('The commit has been successfully reverted.') - end - end - - context 'when the revert failed' do - before do - post(:revert, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - target_branch: 'master', - id: commit.id) - end - - it 'should redirect to the commit page' do - # Reverting a commit that has been already reverted. - post(:revert, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - target_branch: 'master', - id: commit.id) - - expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, commit.id) - expect(flash[:alert]).to match('Sorry, we cannot revert this commit automatically.') - end - end - end - - describe '#cherry_pick' do - context 'when target branch is not provided' do - it 'should render the 404 page' do - post(:cherry_pick, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: master_pickable_commit.id) - - expect(response).not_to be_success - expect(response).to have_http_status(404) - end - end - - context 'when the cherry-pick was successful' do - it 'should redirect to the commits page' do - post(:cherry_pick, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - target_branch: 'master', - id: master_pickable_commit.id) - - expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master') - expect(flash[:notice]).to eq('The commit has been successfully cherry-picked.') - end - end - - context 'when the cherry_pick failed' do - before do - post(:cherry_pick, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - target_branch: 'master', - id: master_pickable_commit.id) - end - - it 'should redirect to the commit page' do - # Cherry-picking a commit that has been already cherry-picked. - post(:cherry_pick, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - target_branch: 'master', - id: master_pickable_commit.id) - - expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) - expect(flash[:alert]).to match('Sorry, we cannot cherry-pick this commit automatically.') - end - end - end -end diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index 6e3db10e451..3001d32e719 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -1,9 +1,29 @@ -require 'rails_helper' +require 'spec_helper' describe Projects::CommitController do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:commit) { project.commit("master") } + let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' } + let(:master_pickable_commit) { project.commit(master_pickable_sha) } + + before do + sign_in(user) + project.team << [user, :master] + end + describe 'GET show' do render_views + def go(extra_params = {}) + params = { + namespace_id: project.namespace.to_param, + project_id: project.to_param + } + + get :show, params.merge(extra_params) + end + let(:project) { create(:project) } before do @@ -15,7 +35,7 @@ describe Projects::CommitController do context 'with valid id' do it 'responds with 200' do - go id: project.commit.id + go(id: commit.id) expect(response).to be_ok end @@ -23,27 +43,274 @@ describe Projects::CommitController do context 'with invalid id' do it 'responds with 404' do - go id: project.commit.id.reverse + go(id: commit.id.reverse) expect(response).to be_not_found end end it 'handles binary files' do - get(:show, + go(id: TestEnv::BRANCH_SHA['binary-encoding'], format: 'html') + + expect(response).to be_success + end + + shared_examples "export as" do |format| + it "should generally work" do + go(id: commit.id, format: format) + + expect(response).to be_success + end + + it "should generate it" do + expect_any_instance_of(Commit).to receive(:"to_#{format}") + + go(id: commit.id, format: format) + end + + it "should render it" do + go(id: commit.id, format: format) + + expect(response.body).to eq(commit.send(:"to_#{format}")) + end + + it "should not escape Html" do + allow_any_instance_of(Commit).to receive(:"to_#{format}"). + and_return('HTML entities &<>" ') + + go(id: commit.id, format: format) + + expect(response.body).not_to include('&') + expect(response.body).not_to include('>') + expect(response.body).not_to include('<') + expect(response.body).not_to include('"') + end + end + + describe "as diff" do + include_examples "export as", :diff + let(:format) { :diff } + + it "should really only be a git diff" do + go(id: commit.id, format: format) + + expect(response.body).to start_with("diff --git") + end + + it "should really only be a git diff without whitespace changes" do + go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format, w: 1) + + expect(response.body).to start_with("diff --git") + # without whitespace option, there are more than 2 diff_splits + diff_splits = assigns(:diffs).first.diff.split("\n") + expect(diff_splits.length).to be <= 2 + end + end + + describe "as patch" do + include_examples "export as", :patch + let(:format) { :patch } + + it "should really be a git email patch" do + go(id: commit.id, format: format) + + expect(response.body).to start_with("From #{commit.id}") + end + + it "should contain a git diff" do + go(id: commit.id, format: format) + + expect(response.body).to match(/^diff --git/) + end + end + + context 'commit that removes a submodule' do + render_views + + let(:fork_project) { create(:forked_project_with_submodules, visibility_level: 20) } + let(:commit) { fork_project.commit('remove-submodule') } + + it 'renders it' do + get(:show, + namespace_id: fork_project.namespace.to_param, + project_id: fork_project.to_param, + id: commit.id) + + expect(response).to be_success + end + end + end + + describe "GET branches" do + it "contains branch and tags information" do + get(:branches, namespace_id: project.namespace.to_param, project_id: project.to_param, - id: TestEnv::BRANCH_SHA['binary-encoding'], - format: "html") + id: commit.id) - expect(response).to be_success + expect(assigns(:branches)).to include("master", "feature_conflict") + expect(assigns(:tags)).to include("v1.1.0") + end + end + + describe 'POST revert' do + context 'when target branch is not provided' do + it 'should render the 404 page' do + post(:revert, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: commit.id) + + expect(response).not_to be_success + expect(response).to have_http_status(404) + end + end + + context 'when the revert was successful' do + it 'should redirect to the commits page' do + post(:revert, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: commit.id) + + expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master') + expect(flash[:notice]).to eq('The commit has been successfully reverted.') + end + end + + context 'when the revert failed' do + before do + post(:revert, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: commit.id) + end + + it 'should redirect to the commit page' do + # Reverting a commit that has been already reverted. + post(:revert, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: commit.id) + + expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, commit.id) + expect(flash[:alert]).to match('Sorry, we cannot revert this commit automatically.') + end + end + end + + describe 'POST cherry_pick' do + context 'when target branch is not provided' do + it 'should render the 404 page' do + post(:cherry_pick, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: master_pickable_commit.id) + + expect(response).not_to be_success + expect(response).to have_http_status(404) + end + end + + context 'when the cherry-pick was successful' do + it 'should redirect to the commits page' do + post(:cherry_pick, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: master_pickable_commit.id) + + expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master') + expect(flash[:notice]).to eq('The commit has been successfully cherry-picked.') + end end - def go(id:) - get :show, + context 'when the cherry_pick failed' do + before do + post(:cherry_pick, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: master_pickable_commit.id) + end + + it 'should redirect to the commit page' do + # Cherry-picking a commit that has been already cherry-picked. + post(:cherry_pick, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: master_pickable_commit.id) + + expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, master_pickable_commit.id) + expect(flash[:alert]).to match('Sorry, we cannot cherry-pick this commit automatically.') + end + end + end + + describe 'GET diff_for_path' do + def diff_for_path(extra_params = {}) + params = { namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: id + project_id: project.to_param + } + + get :diff_for_path, params.merge(extra_params) + end + + let(:existing_path) { '.gitmodules' } + + context 'when the commit exists' do + context 'when the user has access to the project' do + context 'when the path exists in the diff' do + it 'enables diff notes' do + diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path) + + expect(assigns(:diff_notes_disabled)).to be_falsey + expect(assigns(:comments_target)).to eq(noteable_type: 'Commit', + commit_id: commit.id) + end + + it 'only renders the diffs for the path given' do + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project| + expect(diffs.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs, diff_refs, project) + end + + diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path) + end + end + + context 'when the path does not exist in the diff' do + before { diff_for_path(id: commit.id, old_path: existing_path.succ, new_path: existing_path.succ) } + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'when the user does not have access to the project' do + before do + project.team.truncate + diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path) + end + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'when the commit does not exist' do + before { diff_for_path(id: commit.id.succ, old_path: existing_path, new_path: existing_path) } + + it 'returns a 404' do + expect(response).to have_http_status(404) + end end end end diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index 4018dac95a2..4058d5e2453 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -64,4 +64,73 @@ describe Projects::CompareController do expect(assigns(:commits)).to eq(nil) end end + + describe 'GET diff_for_path' do + def diff_for_path(extra_params = {}) + params = { + namespace_id: project.namespace.to_param, + project_id: project.to_param + } + + get :diff_for_path, params.merge(extra_params) + end + + let(:existing_path) { 'files/ruby/feature.rb' } + + context 'when the from and to refs exist' do + context 'when the user has access to the project' do + context 'when the path exists in the diff' do + it 'disables diff notes' do + diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path) + + expect(assigns(:diff_notes_disabled)).to be_truthy + end + + it 'only renders the diffs for the path given' do + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project| + expect(diffs.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs, diff_refs, project) + end + + diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path) + end + end + + context 'when the path does not exist in the diff' do + before { diff_for_path(from: ref_from, to: ref_to, old_path: existing_path.succ, new_path: existing_path.succ) } + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'when the user does not have access to the project' do + before do + project.team.truncate + diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path) + end + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'when the from ref does not exist' do + before { diff_for_path(from: ref_from.succ, to: ref_to, old_path: existing_path, new_path: existing_path) } + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + + context 'when the to ref does not exist' do + before { diff_for_path(from: ref_from, to: ref_to.succ, old_path: existing_path, new_path: existing_path) } + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index c4b57e77804..210085e3b1a 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -10,7 +10,7 @@ describe Projects::MergeRequestsController do project.team << [user, :master] end - describe '#new' do + describe 'GET new' do context 'merge request that removes a submodule' do render_views @@ -34,7 +34,7 @@ describe Projects::MergeRequestsController do end end - describe "#show" do + describe "GET show" do shared_examples "export merge as" do |format| it "should generally work" do get(:show, @@ -108,7 +108,7 @@ describe Projects::MergeRequestsController do end end - describe 'GET #index' do + describe 'GET index' do def get_merge_requests get :index, namespace_id: project.namespace.to_param, @@ -140,7 +140,7 @@ describe Projects::MergeRequestsController do end end - describe 'PUT #update' do + describe 'PUT update' do context 'there is no source project' do let(:project) { create(:project) } let(:fork_project) { create(:forked_project_with_submodules) } @@ -168,7 +168,7 @@ describe Projects::MergeRequestsController do end end - describe 'POST #merge' do + describe 'POST merge' do let(:base_params) do { namespace_id: project.namespace.path, @@ -266,7 +266,7 @@ describe Projects::MergeRequestsController do end end - describe "DELETE #destroy" do + describe "DELETE destroy" do it "denies access to users unless they're admin or project owner" do delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid @@ -290,96 +290,210 @@ describe Projects::MergeRequestsController do end describe 'GET diffs' do - def go(format: 'html') - get :diffs, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: merge_request.iid, - format: format + def go(extra_params = {}) + params = { + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: merge_request.iid + } + + get :diffs, params.merge(extra_params) end - context 'as html' do - it 'renders the diff template' do - go + context 'with default params' do + context 'as html' do + before { go(format: 'html') } - expect(response).to render_template('diffs') + it 'renders the diff template' do + expect(response).to render_template('diffs') + end end - end - context 'as json' do - it 'renders the diffs template to a string' do - go format: 'json' + context 'as json' do + before { go(format: 'json') } - expect(response).to render_template('projects/merge_requests/show/_diffs') - expect(JSON.parse(response.body)).to have_key('html') + it 'renders the diffs template to a string' do + expect(response).to render_template('projects/merge_requests/show/_diffs') + expect(JSON.parse(response.body)).to have_key('html') + end end - end - - context 'with forked projects with submodules' do - render_views - let(:project) { create(:project) } - let(:fork_project) { create(:forked_project_with_submodules) } - let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } + context 'with forked projects with submodules' do + render_views - before do - fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) - fork_project.save - merge_request.reload - end + let(:project) { create(:project) } + let(:fork_project) { create(:forked_project_with_submodules) } + let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } - it 'renders' do - go format: 'json' + before do + fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) + fork_project.save + merge_request.reload + go(format: 'json') + end - expect(response).to be_success - expect(response.body).to have_content('Subproject commit') + it 'renders' do + expect(response).to be_success + expect(response.body).to have_content('Subproject commit') + end end end - end - describe 'GET diffs with ignore_whitespace_change' do - def go(format: 'html') - get :diffs, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: merge_request.iid, - format: format, - w: 1 - end + context 'with ignore_whitespace_change' do + context 'as html' do + before { go(format: 'html', w: 1) } - context 'as html' do - it 'renders the diff template' do - go + it 'renders the diff template' do + expect(response).to render_template('diffs') + end + end + + context 'as json' do + before { go(format: 'json', w: 1) } - expect(response).to render_template('diffs') + it 'renders the diffs template to a string' do + expect(response).to render_template('projects/merge_requests/show/_diffs') + expect(JSON.parse(response.body)).to have_key('html') + end end end - context 'as json' do - it 'renders the diffs template to a string' do - go format: 'json' + context 'with view' do + before { go(view: 'parallel') } - expect(response).to render_template('projects/merge_requests/show/_diffs') - expect(JSON.parse(response.body)).to have_key('html') + it 'saves the preferred diff view in a cookie' do + expect(response.cookies['diff_view']).to eq('parallel') end end end - describe 'GET diffs with view' do - def go(extra_params = {}) + describe 'GET diff_for_path' do + def diff_for_path(extra_params = {}) params = { namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: merge_request.iid + project_id: project.to_param } - get :diffs, params.merge(extra_params) + get :diff_for_path, params.merge(extra_params) end - it 'saves the preferred diff view in a cookie' do - go view: 'parallel' + context 'when an ID param is passed' do + let(:existing_path) { 'files/ruby/popen.rb' } - expect(response.cookies['diff_view']).to eq('parallel') + context 'when the merge request exists' do + context 'when the user can view the merge request' do + context 'when the path exists in the diff' do + it 'enables diff notes' do + diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path) + + expect(assigns(:diff_notes_disabled)).to be_falsey + expect(assigns(:comments_target)).to eq(noteable_type: 'MergeRequest', + noteable_id: merge_request.id) + end + + it 'only renders the diffs for the path given' do + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project| + expect(diffs.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs, diff_refs, project) + end + + diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path) + end + end + + context 'when the path does not exist in the diff' do + before { diff_for_path(id: merge_request.iid, old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb') } + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'when the user cannot view the merge request' do + before do + project.team.truncate + diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path) + end + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'when the merge request does not exist' do + before { diff_for_path(id: merge_request.iid.succ, old_path: existing_path, new_path: existing_path) } + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + + context 'when the merge request belongs to a different project' do + let(:other_project) { create(:empty_project) } + + before do + other_project.team << [user, :master] + diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path, project_id: other_project.to_param) + end + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end + + context 'when source and target params are passed' do + let(:existing_path) { 'files/ruby/feature.rb' } + + context 'when both branches are in the same project' do + it 'disables diff notes' do + diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' }) + + expect(assigns(:diff_notes_disabled)).to be_truthy + end + + it 'only renders the diffs for the path given' do + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project| + expect(diffs.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs, diff_refs, project) + end + + diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' }) + end + end + + context 'when the source branch is in a different project to the target' do + let(:other_project) { create(:project) } + + before { other_project.team << [user, :master] } + + context 'when the path exists in the diff' do + it 'disables diff notes' do + diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) + + expect(assigns(:diff_notes_disabled)).to be_truthy + end + + it 'only renders the diffs for the path given' do + expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs, diff_refs, project| + expect(diffs.map(&:new_path)).to contain_exactly(existing_path) + meth.call(diffs, diff_refs, project) + end + + diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) + end + end + + context 'when the path does not exist in the diff' do + before { diff_for_path(old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb', merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) } + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 5c8ddbebf0d..b682ced75ac 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -2,7 +2,7 @@ FactoryGirl.define do # Project without repository # # Project does not have bare repository. - # Use this factory if you dont need repository in tests + # Use this factory if you don't need repository in tests factory :empty_project, class: 'Project' do sequence(:name) { |n| "project#{n}" } path { name.downcase.gsub(/\s/, '_') } diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb new file mode 100644 index 00000000000..16baf7e9516 --- /dev/null +++ b/spec/features/admin/admin_abuse_reports_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe "Admin::AbuseReports", feature: true, js: true do + let(:user) { create(:user) } + + context 'as an admin' do + describe 'if a user has been reported for abuse' do + before do + create(:abuse_report, user: user) + login_as :admin + end + + describe 'in the abuse report view' do + it "should present a link to the user's profile" do + visit admin_abuse_reports_path + + expect(page).to have_link user.name, href: user_path(user) + end + end + + describe 'in the profile page of the user' do + it 'should show a link to the admin view of the user' do + visit user_path(user) + + expect(page).to have_link '', href: admin_user_path(user) + end + end + end + end +end diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb new file mode 100644 index 00000000000..7cff196c8d9 --- /dev/null +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -0,0 +1,207 @@ +require 'spec_helper' + +feature 'Expand and collapse diffs', js: true, feature: true do + include WaitForAjax + + before do + login_as :admin + project = create(:project) + branch = 'expand-collapse-diffs' + + # Ensure that undiffable.md is in .gitattributes + project.repository.copy_gitattributes(branch) + visit namespace_project_commit_path(project.namespace, project, project.commit(branch)) + execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });') + end + + def file_container(filename) + find("[data-blob-diff-path*='#{filename}']") + end + + # Use define_method instead of let (which is memoized) so that this just works across a + # reload. + # + files = [ + 'small_diff.md', 'large_diff.md', 'large_diff_renamed.md', 'undiffable.md', + 'too_large.md', 'too_large_image.jpg' + ] + + files.each do |file| + define_method(file.split('.').first) { file_container(file) } + end + + context 'visiting a commit with collapsed diffs' do + it 'shows small diffs immediately' do + expect(small_diff).to have_selector('.code') + expect(small_diff).not_to have_selector('.nothing-here-block') + end + + it 'collapses large diffs by default' do + expect(large_diff).not_to have_selector('.code') + expect(large_diff).to have_selector('.nothing-here-block') + end + + it 'collapses large diffs for renamed files by default' do + expect(large_diff_renamed).not_to have_selector('.code') + expect(large_diff_renamed).to have_selector('.nothing-here-block') + expect(large_diff_renamed).to have_selector('.file-title .deletion') + expect(large_diff_renamed).to have_selector('.file-title .addition') + end + + it 'shows non-renderable diffs as such immediately, regardless of their size' do + expect(undiffable).not_to have_selector('.code') + expect(undiffable).to have_selector('.nothing-here-block') + expect(undiffable).to have_content('gitattributes') + end + + it 'does not allow diffs that are larger than the maximum size to be expanded' do + expect(too_large).not_to have_selector('.code') + expect(too_large).to have_selector('.nothing-here-block') + expect(too_large).to have_content('too large') + end + + it 'shows image diffs immediately, regardless of their size' do + expect(too_large_image).not_to have_selector('.nothing-here-block') + expect(too_large_image).to have_selector('.image') + end + + context 'expanding a diff for a renamed file' do + before do + large_diff_renamed.find('.nothing-here-block').click + wait_for_ajax + end + + it 'shows the old content' do + old_line = large_diff_renamed.find('.line_content.old') + + expect(old_line).to have_content('two copies') + end + + it 'shows the new content' do + new_line = large_diff_renamed.find('.line_content.new', match: :prefer_exact) + + expect(new_line).to have_content('three copies') + end + end + + context 'expanding a large diff' do + before do + click_link('large_diff.md') + wait_for_ajax + end + + it 'makes a request to get the content' do + ajax_uris = evaluate_script('ajaxUris') + + expect(ajax_uris).not_to be_empty + expect(ajax_uris.first).to include('large_diff.md') + end + + it 'shows the diff content' do + expect(large_diff).to have_selector('.code') + expect(large_diff).not_to have_selector('.nothing-here-block') + end + + context 'adding a comment to the expanded diff' do + let(:comment_text) { 'A comment' } + + before do + large_diff.find('.line_holder', match: :prefer_exact).hover + large_diff.find('.add-diff-note').click + large_diff.find('.note-textarea').send_keys comment_text + large_diff.find_button('Comment').click + wait_for_ajax + end + + it 'adds the comment' do + expect(large_diff.find('.notes')).to have_content comment_text + end + + context 'reloading the page' do + before { refresh } + + it 'collapses the large diff by default' do + expect(large_diff).not_to have_selector('.code') + expect(large_diff).to have_selector('.nothing-here-block') + end + + context 'expanding the diff' do + before do + click_link('large_diff.md') + wait_for_ajax + end + + it 'shows the diff content' do + expect(large_diff).to have_selector('.code') + expect(large_diff).not_to have_selector('.nothing-here-block') + end + + it 'shows the diff comment' do + expect(large_diff.find('.notes')).to have_content comment_text + end + end + end + end + end + + context 'collapsing an expanded diff' do + before { click_link('small_diff.md') } + + it 'hides the diff content' do + expect(small_diff).not_to have_selector('.code') + expect(small_diff).to have_selector('.nothing-here-block') + end + + context 're-expanding the same diff' do + before { click_link('small_diff.md') } + + it 'shows the diff content' do + expect(small_diff).to have_selector('.code') + expect(small_diff).not_to have_selector('.nothing-here-block') + end + + it 'does not make a new HTTP request' do + expect(evaluate_script('ajaxUris')).to be_empty + end + end + end + end + + context 'expanding all diffs' do + before do + click_link('Expand all') + wait_for_ajax + execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });') + end + + it 'reloads the page with all diffs expanded' do + expect(small_diff).to have_selector('.code') + expect(small_diff).not_to have_selector('.nothing-here-block') + + expect(large_diff).to have_selector('.code') + expect(large_diff).not_to have_selector('.nothing-here-block') + end + + context 'collapsing an expanded diff' do + before { click_link('small_diff.md') } + + it 'hides the diff content' do + expect(small_diff).not_to have_selector('.code') + expect(small_diff).to have_selector('.nothing-here-block') + end + + context 're-expanding the same diff' do + before { click_link('small_diff.md') } + + it 'shows the diff content' do + expect(small_diff).to have_selector('.code') + expect(small_diff).not_to have_selector('.nothing-here-block') + end + + it 'does not make a new HTTP request' do + expect(evaluate_script('ajaxUris')).to be_empty + end + end + end + end +end diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 006a06b8235..4b9b5394b61 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -7,6 +7,7 @@ describe 'Filter issues', feature: true do let!(:user) { create(:user)} let!(:milestone) { create(:milestone, project: project) } let!(:label) { create(:label, project: project) } + let!(:issue1) { create(:issue, project: project) } before do project.team << [user, :master] @@ -196,6 +197,7 @@ describe 'Filter issues', feature: true do page.within '.labels-filter' do click_link 'bug' end + find('.dropdown-menu-close-icon').click page.within '.issues-list' do expect(page).to have_selector('.issue', count: 1) @@ -287,7 +289,7 @@ describe 'Filter issues', feature: true do wait_for_ajax page.within '.issues-list' do - expect(first('.issue')).to have_content('Frontend') + expect(page).to have_content('Frontend') end end end diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb index 6a39c302f55..98ba93b4036 100644 --- a/spec/features/projects/labels/update_prioritization_spec.rb +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -76,7 +76,7 @@ feature 'Prioritize labels', feature: true do expect(page.all('li').last).to have_content('bug') end - visit current_url + refresh wait_for_ajax page.within('.prioritized-labels') do diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb new file mode 100644 index 00000000000..d94dee0c797 --- /dev/null +++ b/spec/features/protected_branches_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +feature 'Projected Branches', feature: true, js: true do + let(:user) { create(:user, :admin) } + let(:project) { create(:project) } + + before { login_as(user) } + + def set_protected_branch_name(branch_name) + find(".js-protected-branch-select").click + find(".dropdown-input-field").set(branch_name) + click_on "Create Protected Branch: #{branch_name}" + end + + describe "explicit protected branches" do + it "allows creating explicit protected branches" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('some-branch') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content('some-branch') } + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.name).to eq('some-branch') + end + + it "displays the last commit on the matching branch if it exists" do + commit = create(:commit, project: project) + project.repository.add_branch(user, 'some-branch', commit.id) + + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('some-branch') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content(commit.id[0..7]) } + end + + it "displays an error message if the named branch does not exist" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('some-branch') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content('branch was removed') } + end + end + + describe "wildcard protected branches" do + it "allows creating protected branches with a wildcard" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('*-stable') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content('*-stable') } + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.name).to eq('*-stable') + end + + it "displays the number of matching branches" do + project.repository.add_branch(user, 'production-stable', 'master') + project.repository.add_branch(user, 'staging-stable', 'master') + + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('*-stable') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content("2 matching branches") } + end + + it "displays all the branches matching the wildcard" do + project.repository.add_branch(user, 'production-stable', 'master') + project.repository.add_branch(user, 'staging-stable', 'master') + project.repository.add_branch(user, 'development', 'master') + create(:protected_branch, project: project, name: "*-stable") + + visit namespace_project_protected_branches_path(project.namespace, project) + click_on "2 matching branches" + + within(".protected-branches-list") do + expect(page).to have_content("production-stable") + expect(page).to have_content("staging-stable") + expect(page).not_to have_content("development") + end + end + end +end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 85923f0a19d..d0a301038c4 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe "Search", feature: true do let(:user) { create(:user) } let(:project) { create(:project, namespace: user.namespace) } + let!(:issue) { create(:issue, project: project, assignee: user) } + let!(:issue2) { create(:issue, project: project, author: user) } before do login_with(user) diff --git a/spec/fixtures/blockquote_fence_after.md b/spec/fixtures/blockquote_fence_after.md new file mode 100644 index 00000000000..2652a842c0e --- /dev/null +++ b/spec/fixtures/blockquote_fence_after.md @@ -0,0 +1,115 @@ +Single `>>>` inside code block: + +``` +# Code +>>> +# Code +``` + +Double `>>>` inside code block: + +```txt +# Code +>>> +# Code +>>> +# Code +``` + +Blockquote outside code block: + +> Quote + +Code block inside blockquote: + +> Quote +> +> ``` +> # Code +> ``` +> +> Quote + +Single `>>>` inside code block inside blockquote: + +> Quote +> +> ``` +> # Code +> >>> +> # Code +> ``` +> +> Quote + +Double `>>>` inside code block inside blockquote: + +> Quote +> +> ``` +> # Code +> >>> +> # Code +> >>> +> # Code +> ``` +> +> Quote + +Single `>>>` inside HTML: + +<pre> +# Code +>>> +# Code +</pre> + +Double `>>>` inside HTML: + +<pre> +# Code +>>> +# Code +>>> +# Code +</pre> + +Blockquote outside HTML: + +> Quote + +HTML inside blockquote: + +> Quote +> +> <pre> +> # Code +> </pre> +> +> Quote + +Single `>>>` inside HTML inside blockquote: + +> Quote +> +> <pre> +> # Code +> >>> +> # Code +> </pre> +> +> Quote + +Double `>>>` inside HTML inside blockquote: + +> Quote +> +> <pre> +> # Code +> >>> +> # Code +> >>> +> # Code +> </pre> +> +> Quote diff --git a/spec/fixtures/blockquote_fence_before.md b/spec/fixtures/blockquote_fence_before.md new file mode 100644 index 00000000000..d52eec72896 --- /dev/null +++ b/spec/fixtures/blockquote_fence_before.md @@ -0,0 +1,131 @@ +Single `>>>` inside code block: + +``` +# Code +>>> +# Code +``` + +Double `>>>` inside code block: + +```txt +# Code +>>> +# Code +>>> +# Code +``` + +Blockquote outside code block: + +>>> +Quote +>>> + +Code block inside blockquote: + +>>> +Quote + +``` +# Code +``` + +Quote +>>> + +Single `>>>` inside code block inside blockquote: + +>>> +Quote + +``` +# Code +>>> +# Code +``` + +Quote +>>> + +Double `>>>` inside code block inside blockquote: + +>>> +Quote + +``` +# Code +>>> +# Code +>>> +# Code +``` + +Quote +>>> + +Single `>>>` inside HTML: + +<pre> +# Code +>>> +# Code +</pre> + +Double `>>>` inside HTML: + +<pre> +# Code +>>> +# Code +>>> +# Code +</pre> + +Blockquote outside HTML: + +>>> +Quote +>>> + +HTML inside blockquote: + +>>> +Quote + +<pre> +# Code +</pre> + +Quote +>>> + +Single `>>>` inside HTML inside blockquote: + +>>> +Quote + +<pre> +# Code +>>> +# Code +</pre> + +Quote +>>> + +Double `>>>` inside HTML inside blockquote: + +>>> +Quote + +<pre> +# Code +>>> +# Code +>>> +# Code +</pre> + +Quote +>>> diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 7ea9fa933f0..4b134a48410 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -31,26 +31,11 @@ describe DiffHelper do end end - describe 'diff_hard_limit_enabled?' do - it 'should return true if param is provided' do - allow(controller).to receive(:params) { { force_show_diff: true } } - expect(diff_hard_limit_enabled?).to be_truthy - end - - it 'should return false if param is not provided' do - expect(diff_hard_limit_enabled?).to be_falsey - end - end - describe 'diff_options' do - it 'should return hard limit for a diff if force diff is true' do + it 'should return hard limit for a diff' do allow(controller).to receive(:params) { { force_show_diff: true } } expect(diff_options).to include(Commit.max_diff_options) end - - it 'should return safe limit for a diff if force diff is false' do - expect(diff_options).not_to include(:max_lines, :max_files) - end end describe 'unfold_bottom_class' do diff --git a/spec/javascripts/fixtures/issues_show.html.haml b/spec/javascripts/fixtures/issues_show.html.haml index 470cabeafbb..06c2ab1e823 100644 --- a/spec/javascripts/fixtures/issues_show.html.haml +++ b/spec/javascripts/fixtures/issues_show.html.haml @@ -1,7 +1,7 @@ :css .hidden { display: none !important; } -.flash-container +.flash-container.flash-container-page .flash-alert .flash-notice diff --git a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb new file mode 100644 index 00000000000..2799249ae3e --- /dev/null +++ b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +describe Banzai::Filter::BlockquoteFenceFilter, lib: true do + include FilterSpecHelper + + it 'converts blockquote fences to blockquote lines' do + content = File.read(Rails.root.join('spec/fixtures/blockquote_fence_before.md')) + expected = File.read(Rails.root.join('spec/fixtures/blockquote_fence_after.md')) + + output = filter(content) + + expect(output).to eq(expected) + end +end diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb index 543b4786d84..ac9c66e2663 100644 --- a/spec/lib/banzai/reference_parser/base_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb @@ -234,4 +234,79 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do to eq([project]) end end + + describe '#collection_objects_for_ids' do + context 'with RequestStore disabled' do + it 'queries the collection directly' do + collection = User.all + + expect(collection).to receive(:where).twice.and_call_original + + 2.times do + expect(subject.collection_objects_for_ids(collection, [user.id])). + to eq([user]) + end + end + end + + context 'with RequestStore enabled' do + before do + cache = Hash.new { |hash, key| hash[key] = {} } + + allow(RequestStore).to receive(:active?).and_return(true) + allow(subject).to receive(:collection_cache).and_return(cache) + end + + it 'queries the collection on the first call' do + expect(subject.collection_objects_for_ids(User, [user.id])). + to eq([user]) + end + + it 'does not query previously queried objects' do + collection = User.all + + expect(collection).to receive(:where).once.and_call_original + + 2.times do + expect(subject.collection_objects_for_ids(collection, [user.id])). + to eq([user]) + end + end + + it 'casts String based IDs to Fixnums before querying objects' do + 2.times do + expect(subject.collection_objects_for_ids(User, [user.id.to_s])). + to eq([user]) + end + end + + it 'queries any additional objects after the first call' do + other_user = create(:user) + + expect(subject.collection_objects_for_ids(User, [user.id])). + to eq([user]) + + expect(subject.collection_objects_for_ids(User, [user.id, other_user.id])). + to eq([user, other_user]) + end + + it 'caches objects on a per collection class basis' do + expect(subject.collection_objects_for_ids(User, [user.id])). + to eq([user]) + + expect(subject.collection_objects_for_ids(Project, [project.id])). + to eq([project]) + end + end + end + + describe '#collection_cache_key' do + it 'returns the cache key for a Class' do + expect(subject.collection_cache_key(Project)).to eq(Project) + end + + it 'returns the cache key for an ActiveRecord::Relation' do + expect(subject.collection_cache_key(Project.all)).to eq(Project) + end + end end diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb new file mode 100644 index 00000000000..a15aa173fbd --- /dev/null +++ b/spec/lib/gitlab/git/hook_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' +require 'fileutils' + +describe Gitlab::Git::Hook, lib: true do + describe "#trigger" do + let(:project) { create(:project) } + let(:user) { create(:user) } + + def create_hook(name) + FileUtils.mkdir_p(File.join(project.repository.path, 'hooks')) + File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f| + f.write('exit 0') + end + end + + def create_failing_hook(name) + FileUtils.mkdir_p(File.join(project.repository.path, 'hooks')) + File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f| + f.write(<<-HOOK) + echo 'regular message from the hook' + echo 'error message from the hook' 1>&2 + exit 1 + HOOK + end + end + + ['pre-receive', 'post-receive', 'update'].each do |hook_name| + + context "when triggering a #{hook_name} hook" do + context "when the hook is successful" do + it "returns success with no errors" do + create_hook(hook_name) + hook = Gitlab::Git::Hook.new(hook_name, project.repository.path) + blank = Gitlab::Git::BLANK_SHA + ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' + + status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + expect(status).to be true + expect(errors).to be_blank + end + end + + context "when the hook is unsuccessful" do + it "returns failure with errors" do + create_failing_hook(hook_name) + hook = Gitlab::Git::Hook.new(hook_name, project.repository.path) + blank = Gitlab::Git::BLANK_SHA + ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' + + status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + expect(status).to be false + expect(errors).to eq("error message from the hook\n") + end + end + end + end + + context "when the hook doesn't exist" do + it "returns success with no errors" do + hook = Gitlab::Git::Hook.new('unknown_hook', project.repository.path) + blank = Gitlab::Git::BLANK_SHA + ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' + + status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + expect(status).to be true + expect(errors).to be_nil + end + end + end +end diff --git a/spec/lib/gitlab/graphs/commits_spec.rb b/spec/lib/gitlab/graphs/commits_spec.rb new file mode 100644 index 00000000000..f5c064303ad --- /dev/null +++ b/spec/lib/gitlab/graphs/commits_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Gitlab::Graphs::Commits, lib: true do + let!(:project) { create(:project, :public, :empty_repo) } + + let!(:commit1) { create(:commit, git_commit: RepoHelpers.sample_commit, project: project, committed_date: Time.now) } + let!(:commit1_yesterday) { create(:commit, git_commit: RepoHelpers.sample_commit, project: project, committed_date: 1.day.ago)} + + let!(:commit2) { create(:commit, git_commit: RepoHelpers.another_sample_commit, project: project, committed_date: Time.now) } + + describe '#commit_per_day' do + context 'when range is only commits from today' do + subject { described_class.new([commit2, commit1]).commit_per_day } + it { is_expected.to eq 2 } + end + end + + context 'when range is only commits from today' do + subject { described_class.new([commit2, commit1]) } + describe '#commit_per_day' do + it { expect(subject.commit_per_day).to eq 2 } + end + + describe '#duration' do + it { expect(subject.duration).to eq 0 } + end + end + + context 'with commits from yesterday and today' do + subject { described_class.new([commit2, commit1_yesterday]) } + describe '#commit_per_day' do + it { expect(subject.commit_per_day).to eq 1 } + end + + describe '#duration' do + it { expect(subject.duration).to eq 1 } + end + end +end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 0b30e8c9b04..7286b0c39c0 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -4208,7 +4208,18 @@ "name": "User 4" }, "events": [ - + { + "id": 529, + "target_type": "Note", + "target_id": 2521, + "title": "test levels", + "data": null, + "project_id": 4, + "created_at": "2016-07-07T14:35:12.128Z", + "updated_at": "2016-07-07T14:35:12.128Z", + "action": 6, + "author_id": 1 + } ] }, { diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index a72aaa44e82..05ffec8ea0a 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -24,11 +24,27 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect(Ci::Pipeline.first.notes).not_to be_empty end - it 'restores the correct event' do + it 'restores the correct event with symbolised data' do restored_project_json expect(Event.where.not(data: nil).first.data[:ref]).not_to be_empty end + + context 'event at forth level of the tree' do + let(:event) { Event.where(title: 'test levels').first } + + before do + restored_project_json + end + + it 'restores the event' do + expect(event).not_to be_nil + end + + it 'event belongs to note, belongs to merge request, belongs to a project' do + expect(event.note.noteable.project).not_to be_nil + end + end end end end diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb index 59024d3290b..2cb74629da8 100644 --- a/spec/lib/gitlab/url_sanitizer_spec.rb +++ b/spec/lib/gitlab/url_sanitizer_spec.rb @@ -45,6 +45,12 @@ describe Gitlab::UrlSanitizer, lib: true do expect(filtered_content).to include("user@server:project.git") end + + it 'returns an empty string for invalid URLs' do + filtered_content = sanitize_url('ssh://') + + expect(filtered_content).to include("repository '' not found") + end end describe '#sanitized_url' do diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb new file mode 100644 index 00000000000..9a637c94fbe --- /dev/null +++ b/spec/models/merge_request_diff_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe MergeRequestDiff, models: true do + describe '#diffs' do + let(:mr) { create(:merge_request, :with_diffs) } + let(:mr_diff) { mr.merge_request_diff } + + context 'when the :ignore_whitespace_change option is set' do + it 'creates a new compare object instead of loading from the DB' do + expect(mr_diff).not_to receive(:load_diffs) + expect(Gitlab::Git::Compare).to receive(:new).and_call_original + + mr_diff.diffs(ignore_whitespace_change: true) + end + end + + context 'when the raw diffs are empty' do + before { mr_diff.update_attributes(st_diffs: '') } + + it 'returns an empty DiffCollection' do + expect(mr_diff.diffs).to be_a(Gitlab::Git::DiffCollection) + expect(mr_diff.diffs).to be_empty + end + end + + context 'when the raw diffs exist' do + it 'returns the diffs' do + expect(mr_diff.diffs).to be_a(Gitlab::Git::DiffCollection) + expect(mr_diff.diffs).not_to be_empty + end + + context 'when the :paths option is set' do + let(:diffs) { mr_diff.diffs(paths: ['files/ruby/popen.rb', 'files/ruby/popen.rb']) } + + it 'only returns diffs that match the (old path, new path) given' do + expect(diffs.map(&:new_path)).to contain_exactly('files/ruby/popen.rb') + end + + it 'uses the diffs from the DB' do + expect(mr_diff).to receive(:load_diffs) + + diffs + end + end + end + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index a4b6ff8f8ad..c8ad7ab3e7f 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -116,6 +116,31 @@ describe MergeRequest, models: true do end end + describe '#diffs' do + let(:merge_request) { build(:merge_request) } + let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } } + + context 'when there are MR diffs' do + it 'delegates to the MR diffs' do + merge_request.merge_request_diff = MergeRequestDiff.new + + expect(merge_request.merge_request_diff).to receive(:diffs).with(options) + + merge_request.diffs(options) + end + end + + context 'when there are no MR diffs' do + it 'delegates to the compare object' do + merge_request.compare = double(:compare) + + expect(merge_request.compare).to receive(:diffs).with(options) + + merge_request.diffs(options) + end + end + end + describe "#mr_and_commit_notes" do let!(:merge_request) { create(:merge_request) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1b434a726dc..5a27ccbab0a 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -449,6 +449,14 @@ describe Project, models: true do it { expect(project.open_branches.map(&:name)).to include('feature') } it { expect(project.open_branches.map(&:name)).not_to include('master') } + + it "includes branches matching a protected branch wildcard" do + expect(project.open_branches.map(&:name)).to include('feature') + + create(:protected_branch, name: 'feat*', project: project) + + expect(Project.find(project.id).open_branches.map(&:name)).to include('feature') + end end describe '#star_count' do @@ -949,15 +957,67 @@ describe Project, models: true do describe '#protected_branch?' do let(:project) { create(:empty_project) } - it 'returns true when a branch is a protected branch' do + it 'returns true when the branch matches a protected branch via direct match' do project.protected_branches.create!(name: 'foo') expect(project.protected_branch?('foo')).to eq(true) end - it 'returns false when a branch is not a protected branch' do + it 'returns true when the branch matches a protected branch via wildcard match' do + project.protected_branches.create!(name: 'production/*') + + expect(project.protected_branch?('production/some-branch')).to eq(true) + end + + it 'returns false when the branch does not match a protected branch via direct match' do expect(project.protected_branch?('foo')).to eq(false) end + + it 'returns false when the branch does not match a protected branch via wildcard match' do + project.protected_branches.create!(name: 'production/*') + + expect(project.protected_branch?('staging/some-branch')).to eq(false) + end + end + + describe "#developers_can_push_to_protected_branch?" do + let(:project) { create(:empty_project) } + + context "when the branch matches a protected branch via direct match" do + it "returns true if 'Developers can Push' is turned on" do + create(:protected_branch, name: "production", project: project, developers_can_push: true) + + expect(project.developers_can_push_to_protected_branch?('production')).to be true + end + + it "returns false if 'Developers can Push' is turned off" do + create(:protected_branch, name: "production", project: project, developers_can_push: false) + + expect(project.developers_can_push_to_protected_branch?('production')).to be false + end + end + + context "when the branch matches a protected branch via wilcard match" do + it "returns true if 'Developers can Push' is turned on" do + create(:protected_branch, name: "production/*", project: project, developers_can_push: true) + + expect(project.developers_can_push_to_protected_branch?('production/some-branch')).to be true + end + + it "returns false if 'Developers can Push' is turned off" do + create(:protected_branch, name: "production/*", project: project, developers_can_push: false) + + expect(project.developers_can_push_to_protected_branch?('production/some-branch')).to be false + end + end + + context "when the branch does not match a protected branch" do + it "returns false" do + create(:protected_branch, name: "production/*", project: project, developers_can_push: true) + + expect(project.developers_can_push_to_protected_branch?('staging/some-branch')).to be false + end + end end describe '#container_registry_path_with_namespace' do diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb index b523834c6e9..8bf0d24a128 100644 --- a/spec/models/protected_branch_spec.rb +++ b/spec/models/protected_branch_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe ProtectedBranch, models: true do + subject { build_stubbed(:protected_branch) } + describe 'Associations' do it { is_expected.to belong_to(:project) } end @@ -12,4 +14,127 @@ describe ProtectedBranch, models: true do it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:name) } end + + describe "#matches?" do + context "when the protected branch setting is not a wildcard" do + let(:protected_branch) { build(:protected_branch, name: "production/some-branch") } + + it "returns true for branch names that are an exact match" do + expect(protected_branch.matches?("production/some-branch")).to be true + end + + it "returns false for branch names that are not an exact match" do + expect(protected_branch.matches?("staging/some-branch")).to be false + end + end + + context "when the protected branch name contains wildcard(s)" do + context "when there is a single '*'" do + let(:protected_branch) { build(:protected_branch, name: "production/*") } + + it "returns true for branch names matching the wildcard" do + expect(protected_branch.matches?("production/some-branch")).to be true + expect(protected_branch.matches?("production/")).to be true + end + + it "returns false for branch names not matching the wildcard" do + expect(protected_branch.matches?("staging/some-branch")).to be false + expect(protected_branch.matches?("production")).to be false + end + end + + context "when the wildcard contains regex symbols other than a '*'" do + let(:protected_branch) { build(:protected_branch, name: "pro.duc.tion/*") } + + it "returns true for branch names matching the wildcard" do + expect(protected_branch.matches?("pro.duc.tion/some-branch")).to be true + end + + it "returns false for branch names not matching the wildcard" do + expect(protected_branch.matches?("production/some-branch")).to be false + expect(protected_branch.matches?("proXducYtion/some-branch")).to be false + end + end + + context "when there are '*'s at either end" do + let(:protected_branch) { build(:protected_branch, name: "*/production/*") } + + it "returns true for branch names matching the wildcard" do + expect(protected_branch.matches?("gitlab/production/some-branch")).to be true + expect(protected_branch.matches?("/production/some-branch")).to be true + expect(protected_branch.matches?("gitlab/production/")).to be true + expect(protected_branch.matches?("/production/")).to be true + end + + it "returns false for branch names not matching the wildcard" do + expect(protected_branch.matches?("gitlabproductionsome-branch")).to be false + expect(protected_branch.matches?("production/some-branch")).to be false + expect(protected_branch.matches?("gitlab/production")).to be false + expect(protected_branch.matches?("production")).to be false + end + end + + context "when there are arbitrarily placed '*'s" do + let(:protected_branch) { build(:protected_branch, name: "pro*duction/*/gitlab/*") } + + it "returns true for branch names matching the wildcard" do + expect(protected_branch.matches?("production/some-branch/gitlab/second-branch")).to be true + expect(protected_branch.matches?("proXYZduction/some-branch/gitlab/second-branch")).to be true + expect(protected_branch.matches?("proXYZduction/gitlab/gitlab/gitlab")).to be true + expect(protected_branch.matches?("proXYZduction//gitlab/")).to be true + expect(protected_branch.matches?("proXYZduction/some-branch/gitlab/")).to be true + expect(protected_branch.matches?("proXYZduction//gitlab/some-branch")).to be true + end + + it "returns false for branch names not matching the wildcard" do + expect(protected_branch.matches?("production/some-branch/not-gitlab/second-branch")).to be false + expect(protected_branch.matches?("prodXYZuction/some-branch/gitlab/second-branch")).to be false + expect(protected_branch.matches?("proXYZduction/gitlab/some-branch/gitlab")).to be false + expect(protected_branch.matches?("proXYZduction/gitlab//")).to be false + expect(protected_branch.matches?("proXYZduction/gitlab/")).to be false + expect(protected_branch.matches?("proXYZduction//some-branch/gitlab")).to be false + end + end + end + end + + describe "#matching" do + context "for direct matches" do + it "returns a list of protected branches matching the given branch name" do + production = create(:protected_branch, name: "production") + staging = create(:protected_branch, name: "staging") + + expect(ProtectedBranch.matching("production")).to include(production) + expect(ProtectedBranch.matching("production")).not_to include(staging) + end + + it "accepts a list of protected branches to search from, so as to avoid a DB call" do + production = build(:protected_branch, name: "production") + staging = build(:protected_branch, name: "staging") + + expect(ProtectedBranch.matching("production")).to be_empty + expect(ProtectedBranch.matching("production", protected_branches: [production, staging])).to include(production) + expect(ProtectedBranch.matching("production", protected_branches: [production, staging])).not_to include(staging) + end + end + + context "for wildcard matches" do + it "returns a list of protected branches matching the given branch name" do + production = create(:protected_branch, name: "production/*") + staging = create(:protected_branch, name: "staging/*") + + expect(ProtectedBranch.matching("production/some-branch")).to include(production) + expect(ProtectedBranch.matching("production/some-branch")).not_to include(staging) + end + + it "accepts a list of protected branches to search from, so as to avoid a DB call" do + production = build(:protected_branch, name: "production/*") + staging = build(:protected_branch, name: "staging/*") + + expect(ProtectedBranch.matching("production/some-branch")).to be_empty + expect(ProtectedBranch.matching("production/some-branch", protected_branches: [production, staging])).to include(production) + expect(ProtectedBranch.matching("production/some-branch", protected_branches: [production, staging])).not_to include(staging) + end + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 328254ed56b..3984b30ddf8 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -446,6 +446,7 @@ describe User, models: true do it { expect(user.can_create_group?).to be_truthy } it { expect(user.can_create_project?).to be_truthy } it { expect(user.first_name).to eq('John') } + it { expect(user.external).to be_falsey } end describe 'with defaults' do @@ -468,6 +469,26 @@ describe User, models: true do expect(user.theme_id).to eq(1) end end + + context 'when current_application_settings.user_default_external is true' do + before do + stub_application_setting(user_default_external: true) + end + + it "creates external user by default" do + user = build(:user) + + expect(user.external).to be_truthy + end + + describe 'with default overrides' do + it "creates a non-external user" do + user = build(:user, external: false) + + expect(user.external).to be_falsey + end + end + end end describe '.find_by_any_email' do diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 04141a45031..c2c94040ece 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -49,10 +49,25 @@ describe API::API, api: true do describe "GET /groups/:id" do context "when authenticated as user" do - it "should return one of user1's groups" do + it "returns one of user1's groups" do + project = create(:project, namespace: group2, path: 'Foo') + create(:project_group_link, project: project, group: group1) + get api("/groups/#{group1.id}", user1) + expect(response).to have_http_status(200) - json_response['name'] == group1.name + expect(json_response['id']).to eq(group1.id) + expect(json_response['name']).to eq(group1.name) + expect(json_response['path']).to eq(group1.path) + expect(json_response['description']).to eq(group1.description) + expect(json_response['visibility_level']).to eq(group1.visibility_level) + expect(json_response['avatar_url']).to eq(group1.avatar_url) + expect(json_response['web_url']).to eq(group1.web_url) + expect(json_response['projects']).to be_an Array + expect(json_response['projects'].length).to eq(2) + expect(json_response['shared_projects']).to be_an Array + expect(json_response['shared_projects'].length).to eq(1) + expect(json_response['shared_projects'][0]['id']).to eq(project.id) end it "should not return a non existing group" do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 611dd2a2a88..8a52725a893 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -392,11 +392,47 @@ describe API::API, api: true do before { project } before { project_member } - it 'should return a project by id' do + it 'returns a project by id' do + group = create(:group) + link = create(:project_group_link, project: project, group: group) + get api("/projects/#{project.id}", user) + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(project.id) + expect(json_response['description']).to eq(project.description) + expect(json_response['default_branch']).to eq(project.default_branch) + expect(json_response['tag_list']).to be_an Array + expect(json_response['public']).to be_falsey + expect(json_response['archived']).to be_falsey + expect(json_response['visibility_level']).to be_present + expect(json_response['ssh_url_to_repo']).to be_present + expect(json_response['http_url_to_repo']).to be_present + expect(json_response['web_url']).to be_present + expect(json_response['owner']).to be_a Hash + expect(json_response['owner']).to be_a Hash expect(json_response['name']).to eq(project.name) - expect(json_response['owner']['username']).to eq(user.username) + expect(json_response['path']).to be_present + expect(json_response['issues_enabled']).to be_present + expect(json_response['merge_requests_enabled']).to be_present + expect(json_response['wiki_enabled']).to be_present + expect(json_response['builds_enabled']).to be_present + expect(json_response['snippets_enabled']).to be_present + expect(json_response['container_registry_enabled']).to be_present + expect(json_response['created_at']).to be_present + expect(json_response['last_activity_at']).to be_present + expect(json_response['shared_runners_enabled']).to be_present + expect(json_response['creator_id']).to be_present + expect(json_response['namespace']).to be_present + expect(json_response['avatar_url']).to be_nil + expect(json_response['star_count']).to be_present + expect(json_response['forks_count']).to be_present + expect(json_response['public_builds']).to be_present + expect(json_response['shared_with_groups']).to be_an Array + expect(json_response['shared_with_groups'].length).to eq(1) + expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) + expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) + expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) end it 'should return a project by path name' do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 54719cbb8d8..d3dddfb4817 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -293,6 +293,30 @@ describe NotificationService, services: true do end end end + + context "merge request diff note" do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request, source_project: project, assignee: user) } + let(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) } + + before do + build_team(note.project) + project.team << [merge_request.author, :master] + project.team << [merge_request.assignee, :master] + end + + describe :new_note do + it "records sent notifications" do + # Ensure create SentNotification by noteable = merge_request 6 times, not noteable = note + expect(SentNotification).to receive(:record_note).with(note, any_args).exactly(4).times.and_call_original + + notification.new_note(note) + + expect(SentNotification.last.position).to eq(note.position) + end + end + end end describe 'Issues' do diff --git a/spec/support/capybara_helpers.rb b/spec/support/capybara_helpers.rb index 9b5c3065eed..b57a3493aff 100644 --- a/spec/support/capybara_helpers.rb +++ b/spec/support/capybara_helpers.rb @@ -27,6 +27,14 @@ module CapybaraHelpers end end end + + # Refresh the page. Calling `visit current_url` doesn't seem to work consistently. + # + def refresh + url = current_url + visit 'about:blank' + visit url + end end RSpec.configure do |config| diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 9f9ef20f99b..bb6c84262f6 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -5,19 +5,20 @@ module TestEnv # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { - 'empty-branch' => '7efb185', - 'flatten-dir' => 'e56497b', - 'feature' => '0b4bc9a', - 'feature_conflict' => 'bb5206f', - 'fix' => '48f0be4', - 'improve/awesome' => '5937ac0', - 'markdown' => '0ed8c6c', - 'lfs' => 'be93687', - 'master' => '5937ac0', - "'test'" => 'e56497b', - 'orphaned-branch' => '45127a9', - 'binary-encoding' => '7b1cf43', - 'gitattributes' => '5a62481', + 'empty-branch' => '7efb185', + 'flatten-dir' => 'e56497b', + 'feature' => '0b4bc9a', + 'feature_conflict' => 'bb5206f', + 'fix' => '48f0be4', + 'improve/awesome' => '5937ac0', + 'markdown' => '0ed8c6c', + 'lfs' => 'be93687', + 'master' => '5937ac0', + "'test'" => 'e56497b', + 'orphaned-branch' => '45127a9', + 'binary-encoding' => '7b1cf43', + 'gitattributes' => '5a62481', + 'expand-collapse-diffs' => '4842455' } # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily @@ -63,7 +64,7 @@ module TestEnv end def disable_pre_receive - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true) + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) end # Clean /tmp/tests |