diff options
author | Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> | 2015-04-27 11:38:36 +0300 |
---|---|---|
committer | Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> | 2015-04-27 11:38:36 +0300 |
commit | 278c3ba401efc68b6454abc2d530d813f2a9ffec (patch) | |
tree | 9a2b8349e71a5dffc069a61fb92e8454a19a587f | |
parent | 2cbdfbfd1edd251fdeab0e238552175644fe18d1 (diff) | |
parent | f3ffd7d0b1d357b1dff6c00f02e3fbcaead7e2b6 (diff) | |
download | gitlab-ce-278c3ba401efc68b6454abc2d530d813f2a9ffec.tar.gz |
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce
55 files changed, 743 insertions, 518 deletions
diff --git a/CHANGELOG b/CHANGELOG index dd42ce23f39..38dcb7dd227 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.11.0 (unreleased) + - Get Gitorious importer to work again. - Fix clone URL field and X11 Primary selection (Dmitry Medvinsky) - Ignore invalid lines in .gitmodules - Fix "Cannot move project" error message from popping up after a successful transfer (Stan Hu) @@ -223,14 +223,13 @@ end group :development, :test do gem 'coveralls', require: false gem 'rubocop', '0.28.0', require: false - # gem 'rails-dev-tweaks' gem 'spinach-rails' gem "rspec-rails", '2.99' - gem "capybara", '~> 2.2.1' + gem 'capybara', '~> 2.2.1' + gem 'capybara-screenshot', '~> 1.0.0' gem "pry-rails" gem "awesome_print" gem "database_cleaner" - gem "launchy" gem 'factory_girl_rails' # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) diff --git a/Gemfile.lock b/Gemfile.lock index f0f8601a760..2260beb0a5e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -85,6 +85,9 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) + capybara-screenshot (1.0.9) + capybara (>= 1.0, < 3) + launchy carrierwave (0.9.0) activemodel (>= 3.2.0) activesupport (>= 3.2.0) @@ -677,6 +680,7 @@ DEPENDENCIES byebug cal-heatmap-rails (~> 0.0.1) capybara (~> 2.2.1) + capybara-screenshot (~> 1.0.0) carrierwave charlock_holmes coffee-rails @@ -726,7 +730,6 @@ DEPENDENCIES jquery-turbolinks jquery-ui-rails kaminari (~> 0.15.1) - launchy letter_opener minitest (~> 5.3.0) mousetrap-rails diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 020c103dbc5..bb9da147018 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -132,10 +132,17 @@ $ -> ), 1 # Initialize tooltips - $('.has_tooltip').tooltip() - - # Bottom tooltip - $('.has_bottom_tooltip').tooltip(placement: 'bottom') + $('body').tooltip({ + selector: '.has_tooltip, [data-toggle="tooltip"], .page-sidebar-collapsed .nav-sidebar a' + placement: (_, el) -> + $el = $(el) + if $el.attr('id') == 'js-shortcuts-home' + # Place the logo tooltip on the right when collapsed, bottom when expanded + $el.parents('header').hasClass('header-collapsed') and 'right' or 'bottom' + else + # Otherwise use the data-placement attribute like normal + $el.data('placement') + }) # Form submitter $('.trigger-submit').on 'change', -> diff --git a/app/assets/stylesheets/base/gl_bootstrap.scss b/app/assets/stylesheets/base/gl_bootstrap.scss index 62a3eade5c7..427f333423c 100644 --- a/app/assets/stylesheets/base/gl_bootstrap.scss +++ b/app/assets/stylesheets/base/gl_bootstrap.scss @@ -117,7 +117,7 @@ color: #888; text-shadow: 0 1px 1px #fff; } - i[class~="fa"] { + i.fa { line-height: 14px; } } diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/base/mixins.scss index feff931156c..b8b163a42b2 100644 --- a/app/assets/stylesheets/base/mixins.scss +++ b/app/assets/stylesheets/base/mixins.scss @@ -106,7 +106,6 @@ p > code { font-size: inherit; font-weight: inherit; - color: #555; } li { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index facd7e19314..97b19deb3ed 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -136,7 +136,7 @@ ul.notes { display: none; float: right; - [class~="fa"] { + i.fa { font-size: 16px; line-height: 16px; vertical-align: middle; diff --git a/app/controllers/import/gitorious_controller.rb b/app/controllers/import/gitorious_controller.rb index 6067a87ee04..c121d2de7cb 100644 --- a/app/controllers/import/gitorious_controller.rb +++ b/app/controllers/import/gitorious_controller.rb @@ -6,7 +6,7 @@ class Import::GitoriousController < Import::BaseController def callback session[:gitorious_repos] = params[:repos] - redirect_to status_import_gitorious_url + redirect_to status_import_gitorious_path end def status diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb new file mode 100644 index 00000000000..e6456198264 --- /dev/null +++ b/app/models/commit_range.rb @@ -0,0 +1,106 @@ +# CommitRange makes it easier to work with commit ranges +# +# Examples: +# +# range = CommitRange.new('f3f85602...e86e1013') +# range.exclude_start? # => false +# range.reference_title # => "Commits f3f85602 through e86e1013" +# range.to_s # => "f3f85602...e86e1013" +# +# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae') +# range.exclude_start? # => true +# range.reference_title # => "Commits f3f85602^ through e86e1013" +# range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"} +# range.to_s # => "f3f85602..e86e1013" +# +# # Assuming `project` is a Project with a repository containing both commits: +# range.project = project +# range.valid_commits? # => true +# +class CommitRange + include ActiveModel::Conversion + + attr_reader :sha_from, :notation, :sha_to + + # Optional Project model + attr_accessor :project + + # See `exclude_start?` + attr_reader :exclude_start + + # The beginning and ending SHA sums can be between 6 and 40 hex characters, + # and the range selection can be double- or triple-dot. + PATTERN = /\h{6,40}\.{2,3}\h{6,40}/ + + # Initialize a CommitRange + # + # range_string - The String commit range. + # project - An optional Project model. + # + # Raises ArgumentError if `range_string` does not match `PATTERN`. + def initialize(range_string, project = nil) + range_string.strip! + + unless range_string.match(/\A#{PATTERN}\z/) + raise ArgumentError, "invalid CommitRange string format: #{range_string}" + end + + @exclude_start = !range_string.include?('...') + @sha_from, @notation, @sha_to = range_string.split(/(\.{2,3})/, 2) + + @project = project + end + + def inspect + %(#<#{self.class}:#{object_id} #{to_s}>) + end + + def to_s + "#{sha_from[0..7]}#{notation}#{sha_to[0..7]}" + end + + # Returns a String for use in a link's title attribute + def reference_title + "Commits #{suffixed_sha_from} through #{sha_to}" + end + + # Return a Hash of parameters for passing to a URL helper + # + # See `namespace_project_compare_url` + def to_param + { from: suffixed_sha_from, to: sha_to } + end + + def exclude_start? + exclude_start + end + + # Check if both the starting and ending commit IDs exist in a project's + # repository + # + # project - An optional Project to check (default: `project`) + def valid_commits?(project = project) + return nil unless project.present? + return false unless project.valid_repo? + + commit_from.present? && commit_to.present? + end + + def persisted? + true + end + + def commit_from + @commit_from ||= project.repository.commit(suffixed_sha_from) + end + + def commit_to + @commit_to ||= project.repository.commit(sha_to) + end + + private + + def suffixed_sha_from + sha_from + (exclude_start? ? '^' : '') + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 59cca01e084..391c9ed6e9d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -686,11 +686,21 @@ class Project < ActiveRecord::Base end def create_repository - if gitlab_shell.add_repository(path_with_namespace) - true + if forked? + if gitlab_shell.fork_repository(forked_from_project.path_with_namespace, self.namespace.path) + ensure_satellite_exists + true + else + errors.add(:base, 'Failed to fork repository') + false + end else - errors.add(:base, 'Failed to create repository') - false + if gitlab_shell.add_repository(path_with_namespace) + true + else + errors.add(:base, 'Failed to create repository') + false + end end end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index a7afcf8f64b..011f6f6145e 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -5,6 +5,8 @@ module Projects end def execute + forked_from_project_id = params.delete(:forked_from_project_id) + @project = Project.new(params) # Make sure that the user is allowed to use the specified visibility @@ -45,10 +47,14 @@ module Projects @project.creator = current_user + if forked_from_project_id + @project.build_forked_project_link(forked_from_project_id: forked_from_project_id) + end + Project.transaction do @project.save - unless @project.import? + if @project.persisted? && !@project.import? unless @project.create_repository raise 'Failed to create repository' end diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 1e4deb6ed39..50f208b11d1 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -1,66 +1,28 @@ module Projects class ForkService < BaseService - include Gitlab::ShellAdapter - def execute - @from_project = @project - - project_params = { - visibility_level: @from_project.visibility_level, - description: @from_project.description, + new_params = { + forked_from_project_id: @project.id, + visibility_level: @project.visibility_level, + description: @project.description, + name: @project.name, + path: @project.path, + namespace_id: @params[:namespace].try(:id) || current_user.namespace.id } - project = Project.new(project_params) - project.name = @from_project.name - project.path = @from_project.path - project.creator = @current_user - if @from_project.avatar.present? && @from_project.avatar.image? - project.avatar = @from_project.avatar - end - - if namespace = @params[:namespace] - project.namespace = namespace - else - project.namespace = @current_user.namespace + if @project.avatar.present? && @project.avatar.image? + new_params[:avatar] = @project.avatar end - unless @current_user.can?(:create_projects, project.namespace) - project.errors.add(:namespace, 'insufficient access rights') - return project - end - - # If the project cannot save, we do not want to trigger the project destroy - # as this can have the side effect of deleting a repo attached to an existing - # project with the same name and namespace - if project.valid? - begin - Project.transaction do - #First save the DB entries as they can be rolled back if the repo fork fails - project.build_forked_project_link(forked_to_project_id: project.id, forked_from_project_id: @from_project.id) - if project.save - project.team << [@current_user, :master, @current_user] - end - - #Now fork the repo - unless gitlab_shell.fork_repository(@from_project.path_with_namespace, project.namespace.path) - raise 'forking failed in gitlab-shell' - end - - project.ensure_satellite_exists - end + new_project = CreateService.new(current_user, new_params).execute - if @from_project.gitlab_ci? - ForkRegistrationWorker.perform_async(@from_project.id, project.id, @current_user.private_token) - end - rescue => ex - project.errors.add(:base, 'Fork transaction failed.') - project.destroy + if new_project.persisted? + if @project.gitlab_ci? + ForkRegistrationWorker.perform_async(@project.id, new_project.id, @current_user.private_token) end - else - project.errors.add(:base, 'Invalid fork destination') end - project + new_project end end end diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index 552525f4a07..c2577a24982 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -18,7 +18,7 @@ %a.twitter-share-button{ | href: "https://twitter.com/share", | "data-url" => event.project.web_url, | - "data-text" => "I just #{event.project.imported? ? "imported" : "created"} a new project in GitLab! GitLab is version control on your server.", | + "data-text" => "I just #{event.action_name} a new project on GitLab! GitLab is version control on your server.", | "data-size" => "medium", | "data-related" => "gitlab", | "data-hashtags" => "gitlab", | diff --git a/app/views/groups/_settings_nav.html.haml b/app/views/groups/_settings_nav.html.haml index e6aee22e529..f93caf90076 100644 --- a/app/views/groups/_settings_nav.html.haml +++ b/app/views/groups/_settings_nav.html.haml @@ -1,11 +1,11 @@ %ul.sidebar-subnav = nav_link(path: 'groups#edit') do - = link_to edit_group_path(@group), title: 'Group' do - %i.fa.fa-pencil-square-o + = link_to edit_group_path(@group), title: 'Group', data: {placement: 'right'} do + = icon('pencil-square-o') %span Group = nav_link(path: 'groups#projects') do - = link_to projects_group_path(@group), title: 'Projects' do - %i.fa.fa-folder + = link_to projects_group_path(@group), title: 'Projects', data: {placement: 'right'} do + = icon('folder') %span Projects diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml index 281ce31fe9c..8d4c7b39b19 100644 --- a/app/views/layouts/_head_panel.html.haml +++ b/app/views/layouts/_head_panel.html.haml @@ -2,48 +2,47 @@ .navbar-inner .container %div.app_logo - = link_to root_path, class: "home has_bottom_tooltip", title: "Dashboard" do + = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do = brand_header_logo %h3 GitLab %h1.title= title - %button.navbar-toggle{"data-target" => ".navbar-collapse", "data-toggle" => "collapse", type: "button"} + %button.navbar-toggle{type: 'button', data: {target: '.navbar-collapse', toggle: 'collapse'}} %span.sr-only Toggle navigation - %i.fa.fa-bars + = icon('bars') .navbar-collapse.collapse %ul.nav.navbar-nav %li.hidden-sm.hidden-xs - = render "layouts/search" + = render 'layouts/search' %li.visible-sm.visible-xs - = link_to search_path, title: "Search", class: 'has_bottom_tooltip', 'data-original-title' => 'Search area' do - %i.fa.fa-search + = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('search') %li - = link_to help_path, title: 'Help', class: 'has_bottom_tooltip', - 'data-original-title' => 'Help' do - %i.fa.fa-question-circle + = link_to help_path, title: 'Help', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('question-circle') %li - = link_to explore_root_path, title: "Explore", class: 'has_bottom_tooltip', 'data-original-title' => 'Public area' do - %i.fa.fa-globe + = link_to explore_root_path, title: 'Explore', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('globe') %li - = link_to user_snippets_path(current_user), title: "Your snippets", class: 'has_bottom_tooltip', 'data-original-title' => 'Your snippets' do - %i.fa.fa-clipboard + = link_to user_snippets_path(current_user), title: 'Your snippets', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('clipboard') - if current_user.is_admin? %li - = link_to admin_root_path, title: "Admin area", class: 'has_bottom_tooltip', 'data-original-title' => 'Admin area' do - %i.fa.fa-cogs + = link_to admin_root_path, title: 'Admin area', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('cogs') - if current_user.can_create_project? %li - = link_to new_project_path, title: "New project", class: 'has_bottom_tooltip', 'data-original-title' => 'New project' do - %i.fa.fa-plus + = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('plus') %li - = link_to profile_path, title: "Profile settings", class: 'has_bottom_tooltip', 'data-original-title' => 'Profile settings"' do - %i.fa.fa-user + = link_to profile_path, title: 'Profile settings', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('user') %li - = link_to destroy_user_session_path, class: "logout", method: :delete, title: "Sign out", class: 'has_bottom_tooltip', 'data-original-title' => 'Sign out' do - %i.fa.fa-sign-out + = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('sign-out') %li.hidden-xs - = link_to current_user, class: "profile-pic has_bottom_tooltip", id: 'profile-pic', 'data-original-title' => 'Your profile' do + = link_to current_user, class: 'profile-pic', id: 'profile-pic', data: {toggle: 'tooltip', placement: 'bottom'} do = image_tag avatar_icon(current_user.email, 60), alt: 'User activity' = render 'shared/outdated_browser' diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 1c164800b0e..0fa2ec9824d 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -15,7 +15,3 @@ = yield = yield :embedded_scripts - -:coffeescript - $('.page-sidebar-collapsed .nav-sidebar a').tooltip placement: "right" - diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 372fe9a6d36..a3191593dae 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -5,60 +5,60 @@ %span Overview = nav_link(controller: :projects) do - = link_to admin_namespaces_projects_path, title: 'Projects' do + = link_to admin_namespaces_projects_path, title: 'Projects', data: {placement: 'right'} do = icon('cube fw') %span Projects = nav_link(controller: :users) do - = link_to admin_users_path, title: 'Users' do + = link_to admin_users_path, title: 'Users', data: {placement: 'right'} do = icon('user fw') %span Users = nav_link(controller: :groups) do - = link_to admin_groups_path, title: 'Groups' do + = link_to admin_groups_path, title: 'Groups', data: {placement: 'right'} do = icon('group fw') %span Groups = nav_link(controller: :deploy_keys) do - = link_to admin_deploy_keys_path, title: 'Deploy Keys' do + = link_to admin_deploy_keys_path, title: 'Deploy Keys', data: {placement: 'right'} do = icon('key fw') %span Deploy Keys = nav_link(controller: :logs) do - = link_to admin_logs_path, title: 'Logs' do + = link_to admin_logs_path, title: 'Logs', data: {placement: 'right'} do = icon('file-text fw') %span Logs = nav_link(controller: :broadcast_messages) do - = link_to admin_broadcast_messages_path, title: 'Broadcast Messages' do + = link_to admin_broadcast_messages_path, title: 'Broadcast Messages', data: {placement: 'right'} do = icon('bullhorn fw') %span Messages = nav_link(controller: :hooks) do - = link_to admin_hooks_path, title: 'Hooks' do + = link_to admin_hooks_path, title: 'Hooks', data: {placement: 'right'} do = icon('external-link fw') %span Hooks = nav_link(controller: :background_jobs) do - = link_to admin_background_jobs_path, title: 'Background Jobs' do + = link_to admin_background_jobs_path, title: 'Background Jobs', data: {placement: 'right'} do = icon('cog fw') %span Background Jobs = nav_link(controller: :applications) do - = link_to admin_applications_path, title: 'Applications' do + = link_to admin_applications_path, title: 'Applications', data: {placement: 'right'} do = icon('cloud fw') %span Applications = nav_link(controller: :services) do - = link_to admin_application_settings_services_path, title: 'Service Templates' do + = link_to admin_application_settings_services_path, title: 'Service Templates', data: {placement: 'right'} do = icon('copy fw') %span Service Templates = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do - = link_to admin_application_settings_path, title: 'Settings' do + = link_to admin_application_settings_path, title: 'Settings', data: {placement: 'right'} do = icon('cogs fw') %span Settings diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index c5997e44370..d46dba4a240 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,38 +1,38 @@ %ul.nav.nav-sidebar = nav_link(path: 'dashboard#show', html_options: {class: 'home'}) do - = link_to root_path, title: 'Home', class: 'shortcuts-activity' do + = link_to root_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do = icon('dashboard fw') %span Your Projects = nav_link(path: 'projects#starred') do - = link_to starred_dashboard_projects_path, title: 'Starred Projects' do + = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do = icon('star fw') %span Starred Projects = nav_link(controller: :groups) do - = link_to dashboard_groups_path, title: 'Groups' do + = link_to dashboard_groups_path, title: 'Groups', data: {placement: 'right'} do = icon('group fw') %span Groups = nav_link(controller: :milestones) do - = link_to dashboard_milestones_path, title: 'Milestones' do + = link_to dashboard_milestones_path, title: 'Milestones', data: {placement: 'right'} do = icon('clock-o fw') %span Milestones = nav_link(path: 'dashboard#issues') do - = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues' do + = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do = icon('exclamation-circle fw') %span Issues %span.count= current_user.assigned_issues.opened.count = nav_link(path: 'dashboard#merge_requests') do - = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests' do + = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do = icon('tasks fw') %span Merge Requests %span.count= current_user.assigned_merge_requests.opened.count = nav_link(controller: :help) do - = link_to help_path, title: 'Help' do + = link_to help_path, title: 'Help', data: {placement: 'right'} do = icon('question-circle fw') %span Help diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml index 38d84cd9b51..66870e84ceb 100644 --- a/app/views/layouts/nav/_explore.html.haml +++ b/app/views/layouts/nav/_explore.html.haml @@ -1,18 +1,18 @@ %ul.nav.nav-sidebar = nav_link(path: 'projects#trending') do - = link_to explore_root_path do + = link_to explore_root_path, title: 'Trending Projects', data: {placement: 'right'} do = icon('comments fw') %span Trending Projects = nav_link(path: 'projects#starred') do - = link_to starred_explore_projects_path do + = link_to starred_explore_projects_path, title: 'Most-starred Projects', data: {placement: 'right'} do = icon('star fw') - %span Most Starred Projects + %span Most-starred Projects = nav_link(path: 'projects#index') do - = link_to explore_projects_path do + = link_to explore_projects_path, title: 'All Projects', data: {placement: 'right'} do = icon('bookmark fw') %span All Projects = nav_link(controller: :groups) do - = link_to explore_groups_path do + = link_to explore_groups_path, title: 'All Groups', data: {placement: 'right'} do = icon('group fw') %span All Groups diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 7cce9ffe3d5..74a8526dbd7 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,38 +1,38 @@ %ul.nav.nav-sidebar = nav_link(path: 'groups#show', html_options: {class: 'home'}) do - = link_to group_path(@group), title: "Home" do + = link_to group_path(@group), title: 'Home', data: {placement: 'right'} do = icon('dashboard fw') %span Activity - if current_user = nav_link(controller: [:group, :milestones]) do - = link_to group_milestones_path(@group), title: 'Milestones' do + = link_to group_milestones_path(@group), title: 'Milestones', data: {placement: 'right'} do = icon('clock-o fw') %span Milestones = nav_link(path: 'groups#issues') do - = link_to issues_group_path(@group), title: 'Issues' do + = link_to issues_group_path(@group), title: 'Issues', data: {placement: 'right'} do = icon('exclamation-circle fw') %span Issues - if current_user %span.count= Issue.opened.of_group(@group).count = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group), title: 'Merge Requests' do + = link_to merge_requests_group_path(@group), title: 'Merge Requests', data: {placement: 'right'} do = icon('tasks fw') %span Merge Requests - if current_user %span.count= MergeRequest.opened.of_group(@group).count = nav_link(controller: [:group_members]) do - = link_to group_group_members_path(@group), title: 'Members' do + = link_to group_group_members_path(@group), title: 'Members', data: {placement: 'right'} do = icon('users fw') %span Members - if can?(current_user, :admin_group, @group) = nav_link(html_options: { class: "#{"active" if group_settings_page?} separate-item" }) do - = link_to edit_group_path(@group), title: 'Settings', class: "tab no-highlight" do + = link_to edit_group_path(@group), title: 'Settings', class: 'tab no-highlight', data: {placement: 'right'} do = icon ('cogs fw') %span Settings diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 9f2932c79cd..31d8ed3ed86 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,50 +1,50 @@ %ul.nav.nav-sidebar = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do - = link_to profile_path, title: "Profile" do + = link_to profile_path, title: 'Profile', data: {placement: 'right'} do = icon('user fw') %span Profile = nav_link(controller: :accounts) do - = link_to profile_account_path, title: 'Account' do + = link_to profile_account_path, title: 'Account', data: {placement: 'right'} do = icon('gear fw') %span Account = nav_link(path: ['profiles#applications', 'applications#edit', 'applications#show', 'applications#new']) do - = link_to applications_profile_path, title: 'Applications' do + = link_to applications_profile_path, title: 'Applications', data: {placement: 'right'} do = icon('cloud fw') %span Applications = nav_link(controller: :emails) do - = link_to profile_emails_path, title: 'Emails' do + = link_to profile_emails_path, title: 'Emails', data: {placement: 'right'} do = icon('envelope-o fw') %span Emails %span.count= current_user.emails.count + 1 - unless current_user.ldap_user? = nav_link(controller: :passwords) do - = link_to edit_profile_password_path, title: 'Password' do + = link_to edit_profile_password_path, title: 'Password', data: {placement: 'right'} do = icon('lock fw') %span Password = nav_link(controller: :notifications) do - = link_to profile_notifications_path, title: 'Notifications' do + = link_to profile_notifications_path, title: 'Notifications', data: {placement: 'right'} do = icon('inbox fw') %span Notifications = nav_link(controller: :keys) do - = link_to profile_keys_path, title: 'SSH Keys' do + = link_to profile_keys_path, title: 'SSH Keys', data: {placement: 'right'} do = icon('key fw') %span SSH Keys %span.count= current_user.keys.count = nav_link(path: 'profiles#design') do - = link_to design_profile_path, title: 'Design' do + = link_to design_profile_path, title: 'Design', data: {placement: 'right'} do = icon('image fw') %span Design = nav_link(path: 'profiles#history') do - = link_to history_profile_path, title: 'History' do + = link_to history_profile_path, title: 'History', data: {placement: 'right'} do = icon('history fw') %span History diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index a2a9d8a340b..01b3d70194f 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,7 +1,7 @@ %ul.project-navigation.nav.nav-sidebar - if @project_settings_nav = nav_link do - = link_to project_path(@project), title: 'Back to project', class: "" do + = link_to project_path(@project), title: 'Back to project', data: {placement: 'right'} do = icon('caret-square-o-left fw') %span Back to project @@ -11,49 +11,49 @@ = render 'projects/settings_nav' - else - = nav_link(path: 'projects#show', html_options: {class: "home"}) do - = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do + = nav_link(path: 'projects#show', html_options: {class: 'home'}) do + = link_to project_path(@project), title: 'Project', class: 'shortcuts-project', data: {placement: 'right'} do = icon('dashboard fw') %span Project - if project_nav_tab? :files = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do - = link_to namespace_project_tree_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Files', class: 'shortcuts-tree' do + = link_to namespace_project_tree_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Files', class: 'shortcuts-tree', data: {placement: 'right'} do = icon('files-o fw') %span Files - if project_nav_tab? :commits = nav_link(controller: %w(commit commits compare repositories tags branches)) do - = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Commits', class: 'shortcuts-commits' do + = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Commits', class: 'shortcuts-commits', data: {placement: 'right'} do = icon('history fw') %span Commits - if project_nav_tab? :network = nav_link(controller: %w(network)) do - = link_to namespace_project_network_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Network', class: 'shortcuts-network' do + = link_to namespace_project_network_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Network', class: 'shortcuts-network', data: {placement: 'right'} do = icon('code-fork fw') %span Network - if project_nav_tab? :graphs = nav_link(controller: %w(graphs)) do - = link_to namespace_project_graph_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Graphs', class: 'shortcuts-graphs' do + = link_to namespace_project_graph_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Graphs', class: 'shortcuts-graphs', data: {placement: 'right'} do = icon('area-chart fw') %span Graphs - if project_nav_tab? :milestones = nav_link(controller: :milestones) do - = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do + = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones', data: {placement: 'right'} do = icon('clock-o fw') %span Milestones - if project_nav_tab? :issues = nav_link(controller: :issues) do - = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do + = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do = icon('exclamation-circle fw') %span Issues @@ -62,7 +62,7 @@ - if project_nav_tab? :merge_requests = nav_link(controller: :merge_requests) do - = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do = icon('tasks fw') %span Merge Requests @@ -70,28 +70,28 @@ - if project_nav_tab? :labels = nav_link(controller: :labels) do - = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do + = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels', data: {placement: 'right'} do = icon('tags fw') %span Labels - if project_nav_tab? :wiki = nav_link(controller: :wikis) do - = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do + = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki', data: {placement: 'right'} do = icon('book fw') %span Wiki - if project_nav_tab? :snippets = nav_link(controller: :snippets) do - = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do + = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets', data: {placement: 'right'} do = icon('file-text-o fw') %span Snippets - if project_nav_tab? :settings = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do - = link_to edit_project_path(@project), title: 'Settings', class: "stat-tab tab no-highlight" do + = link_to edit_project_path(@project), title: 'Settings', class: 'stat-tab tab no-highlight', data: {placement: 'right'} do = icon('cogs fw') %span Settings diff --git a/app/views/layouts/nav/_snippets.html.haml b/app/views/layouts/nav/_snippets.html.haml index edd05f2dd81..0de3a9e5bb7 100644 --- a/app/views/layouts/nav/_snippets.html.haml +++ b/app/views/layouts/nav/_snippets.html.haml @@ -1,11 +1,11 @@ %ul.nav.nav-sidebar = nav_link(path: user_snippets_path(current_user), html_options: {class: 'home'}) do - = link_to user_snippets_path(current_user), title: 'Your snippets' do + = link_to user_snippets_path(current_user), title: 'Your snippets', data: {placement: 'right'} do = icon('dashboard fw') %span Your Snippets = nav_link(path: snippets_path) do - = link_to snippets_path, title: 'Discover snippets' do + = link_to snippets_path, title: 'Discover snippets', data: {placement: 'right'} do = icon('globe fw') %span Discover Snippets diff --git a/app/views/profiles/update.js.erb b/app/views/profiles/update.js.erb index e664ac2a52a..db37619136d 100644 --- a/app/views/profiles/update.js.erb +++ b/app/views/profiles/update.js.erb @@ -1,9 +1,3 @@ // Remove body class for any previous theme, re-add current one -$('body').removeClass('ui_basic ui_mars ui_modern ui_gray ui_color light_theme dark_theme') +$('body').removeClass('<%= Gitlab::Theme.body_classes %>') $('body').addClass('<%= app_theme %> <%= theme_type %>') - -// Re-render the header to reflect the new theme -$('header').html('<%= escape_javascript(render("layouts/head_panel", title: "Profile")) %>') - -// Re-initialize header tooltips -$('.has_bottom_tooltip').tooltip({placement: 'bottom'}) diff --git a/app/views/projects/_settings_nav.html.haml b/app/views/projects/_settings_nav.html.haml index 281a84a3d3c..f8b74809b76 100644 --- a/app/views/projects/_settings_nav.html.haml +++ b/app/views/projects/_settings_nav.html.haml @@ -1,31 +1,31 @@ %ul.project-settings-nav.sidebar-subnav = nav_link(path: 'projects#edit') do - = link_to edit_project_path(@project), title: 'Project', class: "stat-tab tab " do - %i.fa.fa-pencil-square-o + = link_to edit_project_path(@project), title: 'Project', class: 'stat-tab tab', data: {placement: 'right'} do + = icon('pencil-square-o') %span Project = nav_link(controller: [:project_members, :teams]) do - = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: "team-tab tab" do - %i.fa.fa-users + = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab', data: {placement: 'right'} do + = icon('users') %span Members = nav_link(controller: :deploy_keys) do - = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do - %i.fa.fa-key + = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys', data: {placement: 'right'} do + = icon('key') %span Deploy Keys = nav_link(controller: :hooks) do - = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks' do - %i.fa.fa-link + = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks', data: {placement: 'right'} do + = icon('link') %span Web Hooks = nav_link(controller: :services) do - = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do - %i.fa.fa-cogs + = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services', data: {placement: 'right'} do + = icon('cogs') %span Services = nav_link(controller: :protected_branches) do - = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do - %i.fa.fa-lock + = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches', data: {placement: 'right'} do + = icon('lock') %span Protected branches diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb index 5e588ceb780..93456a81ecf 100644 --- a/features/steps/dashboard/new_project.rb +++ b/features/steps/dashboard/new_project.rb @@ -4,7 +4,9 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps include SharedProject step 'I click "New project" link' do - click_link "New project" + within('.content') do + click_link "New project" + end end step 'I see "New project" page' do diff --git a/features/support/capybara.rb b/features/support/capybara.rb new file mode 100644 index 00000000000..31dbf0feb2f --- /dev/null +++ b/features/support/capybara.rb @@ -0,0 +1,24 @@ +require 'spinach/capybara' +require 'capybara/poltergeist' + +# Give CI some extra time +timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10 + +Capybara.javascript_driver = :poltergeist +Capybara.register_driver :poltergeist do |app| + Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout) +end + +Spinach.hooks.on_tag("javascript") do + Capybara.current_driver = Capybara.javascript_driver +end + +Capybara.default_wait_time = timeout +Capybara.ignore_hidden_elements = false + +unless ENV['CI'] || ENV['CI_SERVER'] + require 'capybara-screenshot/spinach' + + # Keep only the screenshots generated from the last failing test suite + Capybara::Screenshot.prune_strategy = :keep_last_run +end diff --git a/features/support/db_cleaner.rb b/features/support/db_cleaner.rb new file mode 100644 index 00000000000..1ab308cfa55 --- /dev/null +++ b/features/support/db_cleaner.rb @@ -0,0 +1,11 @@ +require 'database_cleaner' + +DatabaseCleaner.strategy = :truncation + +Spinach.hooks.before_scenario do + DatabaseCleaner.start +end + +Spinach.hooks.after_scenario do + DatabaseCleaner.clean +end diff --git a/features/support/env.rb b/features/support/env.rb index be17065ccfd..f34302721ed 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -11,40 +11,18 @@ ENV['RAILS_ENV'] = 'test' require './config/environment' require 'rspec' require 'rspec/expectations' -require 'database_cleaner' -require 'spinach/capybara' require 'sidekiq/testing/inline' +require_relative 'capybara' +require_relative 'db_cleaner' + %w(select2_helper test_env repo_helpers).each do |f| require Rails.root.join('spec', 'support', f) end -Dir["#{Rails.root}/features/steps/shared/*.rb"].each {|file| require file} +Dir["#{Rails.root}/features/steps/shared/*.rb"].each { |file| require file } WebMock.allow_net_connect! -# -# JS driver -# -require 'capybara/poltergeist' -Capybara.javascript_driver = :poltergeist -Capybara.register_driver :poltergeist do |app| - Capybara::Poltergeist::Driver.new(app, js_errors: false, timeout: 90) -end -Spinach.hooks.on_tag("javascript") do - ::Capybara.current_driver = ::Capybara.javascript_driver -end -Capybara.default_wait_time = 60 -Capybara.ignore_hidden_elements = false - -DatabaseCleaner.strategy = :truncation - -Spinach.hooks.before_scenario do - DatabaseCleaner.start -end - -Spinach.hooks.after_scenario do - DatabaseCleaner.clean -end Spinach.hooks.before_run do include RSpec::Mocks::ExampleMethods diff --git a/lib/gitlab/gitorious_import/client.rb b/lib/gitlab/gitorious_import/client.rb index 8cdc3d4afae..1fa89dba448 100644 --- a/lib/gitlab/gitorious_import/client.rb +++ b/lib/gitlab/gitorious_import/client.rb @@ -14,7 +14,7 @@ module Gitlab end def repos - @repos ||= repo_names.map { |full_name| Repository.new(full_name) } + @repos ||= repo_names.map { |full_name| GitoriousImport::Repository.new(full_name) } end def repo(id) diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb index baa97bee9bf..8764f7e474f 100644 --- a/lib/gitlab/markdown/commit_range_reference_filter.rb +++ b/lib/gitlab/markdown/commit_range_reference_filter.rb @@ -32,11 +32,8 @@ module Gitlab # Pattern used to extract commit range references from text # - # The beginning and ending SHA1 sums can be between 6 and 40 hex - # characters, and the range selection can be double- or triple-dot. - # # This pattern supports cross-project references. - COMMIT_RANGE_PATTERN = /(#{PROJECT_PATTERN}@)?(?<commit_range>\h{6,40}\.{2,3}\h{6,40})/ + COMMIT_RANGE_PATTERN = /(#{PROJECT_PATTERN}@)?(?<commit_range>#{CommitRange::PATTERN})/ def call replace_text_nodes_matching(COMMIT_RANGE_PATTERN) do |content| @@ -53,52 +50,34 @@ module Gitlab # links have `gfm` and `gfm-commit_range` class names attached for # styling. def commit_range_link_filter(text) - self.class.references_in(text) do |match, commit_range, project_ref| + self.class.references_in(text) do |match, id, project_ref| project = self.project_from_ref(project_ref) - from_id, to_id = split_commit_range(commit_range) + range = CommitRange.new(id, project) + + if range.valid_commits? + push_result(:commit_range, range) - if valid_range?(project, from_id, to_id) - url = url_for_commit_range(project, from_id, to_id) + url = url_for_commit_range(project, range) - title = "Commits #{from_id} through #{to_id}" + title = range.reference_title klass = reference_class(:commit_range) project_ref += '@' if project_ref %(<a href="#{url}" title="#{title}" - class="#{klass}">#{project_ref}#{commit_range}</a>) + class="#{klass}">#{project_ref}#{range}</a>) else match end end end - def split_commit_range(range) - from_id, to_id = range.split(/\.{2,3}/, 2) - from_id << "^" if range !~ /\.{3}/ - - [from_id, to_id] - end - - def commit(id) - unless @commit_map[id] - @commit_map[id] = project.commit(id) - end - - @commit_map[id] - end - - def valid_range?(project, from_id, to_id) - project && project.valid_repo? && commit(from_id) && commit(to_id) - end - - def url_for_commit_range(project, from_id, to_id) + def url_for_commit_range(project, range) h = Rails.application.routes.url_helpers h.namespace_project_compare_url(project.namespace, project, - from: from_id, to: to_id, - only_path: context[:only_path]) + range.to_param.merge(only_path: context[:only_path])) end end end diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb index 66598127f6e..b20b29f5d0c 100644 --- a/lib/gitlab/markdown/commit_reference_filter.rb +++ b/lib/gitlab/markdown/commit_reference_filter.rb @@ -48,6 +48,8 @@ module Gitlab project = self.project_from_ref(project_ref) if commit = commit_from_ref(project, commit_ref) + push_result(:commit, commit) + url = url_for_commit(project, commit) title = escape_once(commit.link_title) @@ -57,7 +59,7 @@ module Gitlab %(<a href="#{url}" title="#{title}" - class="#{klass}">#{project_ref}#{commit_ref}</a>) + class="#{klass}">#{project_ref}#{commit.short_id}</a>) else match end diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/issue_reference_filter.rb index c9267cc3e9d..4b360369d37 100644 --- a/lib/gitlab/markdown/issue_reference_filter.rb +++ b/lib/gitlab/markdown/issue_reference_filter.rb @@ -48,6 +48,9 @@ module Gitlab project = self.project_from_ref(project_ref) if project && project.issue_exists?(issue) + # FIXME (rspeicher): Law of Demeter + push_result(:issue, project.issues.where(iid: issue).first) + url = url_for_issue(issue, project, only_path: context[:only_path]) title = escape_once("Issue: #{title_for_issue(issue, project)}") diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb index 4c21192c0d3..a357f28458d 100644 --- a/lib/gitlab/markdown/label_reference_filter.rb +++ b/lib/gitlab/markdown/label_reference_filter.rb @@ -52,11 +52,13 @@ module Gitlab params = label_params(id, name) if label = project.labels.find_by(params) - url = url_for_label(project, label) + push_result(:label, label) + url = url_for_label(project, label) klass = reference_class(:label) - %(<a href="#{url}" class="#{klass}">#{render_colored_label(label)}</a>) + %(<a href="#{url}" + class="#{klass}">#{render_colored_label(label)}</a>) else match end diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb index 40239523cda..7c28fe112ef 100644 --- a/lib/gitlab/markdown/merge_request_reference_filter.rb +++ b/lib/gitlab/markdown/merge_request_reference_filter.rb @@ -48,6 +48,8 @@ module Gitlab project = self.project_from_ref(project_ref) if project && merge_request = project.merge_requests.find_by(iid: id) + push_result(:merge_request, merge_request) + title = escape_once("Merge Request: #{merge_request.title}") klass = reference_class(:merge_request) diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index ef4aa408a7e..a4303d96bef 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -12,7 +12,15 @@ module Gitlab # :reference_class - Custom CSS class added to reference links. # :only_path - Generate path-only links. # + # Results: + # :references - A Hash of references that were found and replaced. class ReferenceFilter < HTML::Pipeline::Filter + def initialize(*args) + super + + result[:references] = Hash.new { |hash, type| hash[type] = [] } + end + def escape_once(html) ERB::Util.html_escape_once(html) end @@ -29,6 +37,16 @@ module Gitlab context[:project] end + # Add a reference to the pipeline's result Hash + # + # type - Singular Symbol reference type (e.g., :issue, :user, etc.) + # values - One or more Objects to add + def push_result(type, *values) + return if values.empty? + + result[:references][type].push(*values) + end + def reference_class(type) "gfm gfm-#{type} #{context[:reference_class]}".strip end diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb index ada67de992b..64a0a2696f7 100644 --- a/lib/gitlab/markdown/snippet_reference_filter.rb +++ b/lib/gitlab/markdown/snippet_reference_filter.rb @@ -48,6 +48,8 @@ module Gitlab project = self.project_from_ref(project_ref) if project && snippet = project.snippets.find_by(id: id) + push_result(:snippet, snippet) + title = escape_once("Snippet: #{snippet.title}") klass = reference_class(:snippet) diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb index 5fc8ed55fe2..28ec041b1d4 100644 --- a/lib/gitlab/markdown/user_reference_filter.rb +++ b/lib/gitlab/markdown/user_reference_filter.rb @@ -38,27 +38,11 @@ module Gitlab # Returns a String with `@user` references replaced with links. All links # have `gfm` and `gfm-project_member` class names attached for styling. def user_link_filter(text) - project = context[:project] - - self.class.references_in(text) do |match, user| - klass = reference_class(:project_member) - - if user == 'all' - url = link_to_all(project) - - %(<a href="#{url}" class="#{klass}">@#{user}</a>) - elsif namespace = Namespace.find_by(path: user) - if namespace.is_a?(Group) - if user_can_reference_group?(namespace) - url = group_url(user, only_path: context[:only_path]) - %(<a href="#{url}" class="#{klass}">@#{user}</a>) - else - match - end - else - url = user_url(user, only_path: context[:only_path]) - %(<a href="#{url}" class="#{klass}">@#{user}</a>) - end + self.class.references_in(text) do |match, username| + if username == 'all' + link_to_all + elsif namespace = Namespace.find_by(path: username) + link_to_namespace(namespace) || match else match end @@ -71,17 +55,46 @@ module Gitlab Rails.application.routes.url_helpers end - def group_url(*args) - urls.group_url(*args) + def link_class + reference_class(:project_member) + end + + def link_to_all + project = context[:project] + + # FIXME (rspeicher): Law of Demeter + push_result(:user, *project.team.members.flatten) + + url = urls.namespace_project_url(project.namespace, project, + only_path: context[:only_path]) + + %(<a href="#{url}" class="#{link_class}">@all</a>) + end + + def link_to_namespace(namespace) + if namespace.is_a?(Group) + link_to_group(namespace.path, namespace) + else + link_to_user(namespace.path, namespace) + end end - def user_url(*args) - urls.user_url(*args) + def link_to_group(group, namespace) + return unless user_can_reference_group?(namespace) + + push_result(:user, *namespace.users) + + url = urls.group_url(group, only_path: context[:only_path]) + + %(<a href="#{url}" class="#{link_class}">@#{group}</a>) end - def link_to_all(project) - urls.namespace_project_url(project.namespace, project, - only_path: context[:only_path]) + def link_to_user(user, namespace) + push_result(:user, namespace.owner) + + url = urls.user_url(user, only_path: context[:only_path]) + + %(<a href="#{url}" class="#{link_class}">@#{user}</a>) end def user_can_reference_group?(group) diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 949dd5d26b1..e35f848fa6e 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -8,151 +8,70 @@ module Gitlab @current_user = current_user end - def can?(user, action, subject) - Ability.abilities.allowed?(user, action, subject) - end - def analyze(text) - text = text.dup - - # Remove preformatted/code blocks so that references are not included - text.gsub!(/^```.*?^```/m, '') - text.gsub!(/[^`]`[^`]*?`[^`]/, '') - - @references = Hash.new { |hash, type| hash[type] = [] } - parse_references(text) + @_text = text.dup end - # Given a valid project, resolve the extracted identifiers of the requested type to - # model objects. - def users - references[:user].uniq.map do |project, identifier| - if identifier == "all" - project.team.members.flatten - elsif namespace = Namespace.find_by(path: identifier) - if namespace.is_a?(Group) - namespace.users if can?(current_user, :read_group, namespace) - else - namespace.owner - end - end - end.flatten.compact.uniq + result = pipeline_result(:user) + result.uniq end def labels - references[:label].uniq.map do |project, identifier| - project.labels.where(id: identifier).first - end.compact.uniq + result = pipeline_result(:label) + result.uniq end def issues - references[:issue].uniq.map do |project, identifier| - if project.default_issues_tracker? - project.issues.where(iid: identifier).first - end - end.compact.uniq + # TODO (rspeicher): What about external issues? + + result = pipeline_result(:issue) + result.uniq end def merge_requests - references[:merge_request].uniq.map do |project, identifier| - project.merge_requests.where(iid: identifier).first - end.compact.uniq + result = pipeline_result(:merge_request) + result.uniq end def snippets - references[:snippet].uniq.map do |project, identifier| - project.snippets.where(id: identifier).first - end.compact.uniq + result = pipeline_result(:snippet) + result.uniq end def commits - references[:commit].uniq.map do |project, identifier| - repo = project.repository - repo.commit(identifier) if repo - end.compact.uniq + result = pipeline_result(:commit) + result.uniq end def commit_ranges - references[:commit_range].uniq.map do |project, identifier| - repo = project.repository - if repo - from_id, to_id = identifier.split(/\.{2,3}/, 2) - [repo.commit(from_id), repo.commit(to_id)] - end - end.compact.uniq + result = pipeline_result(:commit_range) + result.uniq end private - NAME_STR = Gitlab::Regex::NAMESPACE_REGEX_STR - PROJ_STR = "(?<project>#{NAME_STR}/#{NAME_STR})" - - REFERENCE_PATTERN = %r{ - (?<prefix>\W)? # Prefix - ( # Reference - @(?<user>#{NAME_STR}) # User name - |~(?<label>\d+) # Label ID - |(?<issue>([A-Z\-]+-)\d+) # JIRA Issue ID - |#{PROJ_STR}?\#(?<issue>([a-zA-Z\-]+-)?\d+) # Issue ID - |#{PROJ_STR}?!(?<merge_request>\d+) # MR ID - |\$(?<snippet>\d+) # Snippet ID - |(#{PROJ_STR}@)?(?<commit_range>[\h]{6,40}\.{2,3}[\h]{6,40}) # Commit range - |(#{PROJ_STR}@)?(?<commit>[\h]{6,40}) # Commit ID - ) - (?<suffix>\W)? # Suffix - }x.freeze - - TYPES = %i(user issue label merge_request snippet commit commit_range).freeze - - def parse_references(text, project = @project) - # parse reference links - text.gsub!(REFERENCE_PATTERN) do |match| - type = TYPES.detect { |t| $~[t].present? } - - actual_project = project - project_prefix = nil - project_path = $LAST_MATCH_INFO[:project] - if project_path - actual_project = ::Project.find_with_namespace(project_path) - actual_project = nil unless can?(current_user, :read_project, actual_project) - project_prefix = project_path - end - - parse_result($LAST_MATCH_INFO, type, - actual_project, project_prefix) || match - end - end - - # Called from #parse_references. Attempts to build a gitlab reference - # link. Returns nil if +type+ is nil, if the match string is an HTML - # entity, if the reference is invalid, or if the matched text includes an - # invalid project path. - def parse_result(match_info, type, project, project_prefix) - prefix = match_info[:prefix] - suffix = match_info[:suffix] - - return nil if html_entity?(prefix, suffix) || type.nil? - return nil if project.nil? && !project_prefix.nil? - - identifier = match_info[type] - ref_link = reference_link(type, identifier, project, project_prefix) - - if ref_link - "#{prefix}#{ref_link}#{suffix}" - else - nil - end - end - - # Return true if the +prefix+ and +suffix+ indicate that the matched string - # is an HTML entity like & - def html_entity?(prefix, suffix) - prefix && suffix && prefix[0] == '&' && suffix[-1] == ';' - end - - def reference_link(type, identifier, project, _) - references[type] << [project, identifier] + # Instantiate and call HTML::Pipeline with a single reference filter type, + # returning the result + # + # filter_type - Symbol reference type (e.g., :commit, :issue, etc.) + # + # Returns the results Array for the requested filter type + def pipeline_result(filter_type) + klass = filter_type.to_s.camelize + 'ReferenceFilter' + filter = "Gitlab::Markdown::#{klass}".constantize + + context = { + project: project, + current_user: current_user, + # We don't actually care about the links generated + only_path: true + } + + pipeline = HTML::Pipeline.new([filter], context) + result = pipeline.call(@_text) + + result[:references][filter_type] end end end diff --git a/lib/gitlab/theme.rb b/lib/gitlab/theme.rb index 43093c7d27e..f0e61aa2e81 100644 --- a/lib/gitlab/theme.rb +++ b/lib/gitlab/theme.rb @@ -7,33 +7,44 @@ module Gitlab COLOR = 5 unless const_defined?(:COLOR) BLUE = 6 unless const_defined?(:BLUE) - def self.css_class_by_id(id) - themes = { - BASIC => "ui_basic", - MARS => "ui_mars", - MODERN => "ui_modern", - GRAY => "ui_gray", - COLOR => "ui_color", - BLUE => "ui_blue" + def self.classes + @classes ||= { + BASIC => 'ui_basic', + MARS => 'ui_mars', + MODERN => 'ui_modern', + GRAY => 'ui_gray', + COLOR => 'ui_color', + BLUE => 'ui_blue' } + end + def self.css_class_by_id(id) id ||= Gitlab.config.gitlab.default_theme - - themes[id] + classes[id] end - def self.type_css_class_by_id(id) - types = { + def self.types + @types ||= { BASIC => 'light_theme', MARS => 'dark_theme', MODERN => 'dark_theme', GRAY => 'dark_theme', - COLOR => 'dark_theme' + COLOR => 'dark_theme', + BLUE => 'light_theme' } + end + def self.type_css_class_by_id(id) id ||= Gitlab.config.gitlab.default_theme - types[id] end + + # Convenience method to get a space-separated String of all the theme + # classes that mighty be applied to the `body` element + # + # Returns a String + def self.body_classes + (classes.values + types.values).uniq.join(' ') + end end end diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb index f0804ce0056..7274cb309a0 100644 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -42,13 +42,17 @@ module Gitlab::Markdown reference = "#{commit1.short_id}...#{commit2.id}" reference2 = "#{commit1.id}...#{commit2.short_id}" - expect(filter("See #{reference}").css('a').first.text).to eq reference - expect(filter("See #{reference2}").css('a').first.text).to eq reference2 + exp = commit1.short_id + '...' + commit2.short_id + + expect(filter("See #{reference}").css('a').first.text).to eq exp + expect(filter("See #{reference2}").css('a').first.text).to eq exp end it 'links with adjacent text' do doc = filter("See (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + + exp = Regexp.escape("#{commit1.short_id}...#{commit2.short_id}") + expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) end it 'ignores invalid commit IDs' do @@ -81,6 +85,11 @@ module Gitlab::Markdown expect(link).not_to match %r(https?://) expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true) end + + it 'adds to the results hash' do + result = pipeline_result("See #{reference}") + expect(result[:references][:commit_range]).not_to be_empty + end end context 'cross-project reference' do @@ -102,7 +111,9 @@ module Gitlab::Markdown it 'links with adjacent text' do doc = filter("Fixed (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + + exp = Regexp.escape("#{project2.path_with_namespace}@#{commit1.short_id}...#{commit2.short_id}") + expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) end it 'ignores invalid commit IDs on the referenced project' do @@ -112,6 +123,11 @@ module Gitlab::Markdown exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id}...#{commit2.id.reverse}" expect(filter(act).to_html).to eq exp end + + it 'adds to the results hash' do + result = pipeline_result("See #{reference}") + expect(result[:references][:commit_range]).not_to be_empty + end end context 'when user cannot access reference' do diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb index f792d7f696e..cc32a4fcf03 100644 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -27,15 +27,23 @@ module Gitlab::Markdown it "links to a valid reference of #{size} characters" do doc = filter("See #{reference[0...size]}") - expect(doc.css('a').first.text).to eq reference[0...size] + expect(doc.css('a').first.text).to eq commit.short_id expect(doc.css('a').first.attr('href')). to eq urls.namespace_project_commit_url(project.namespace, project, reference) end end + it 'always uses the short ID as the link text' do + doc = filter("See #{commit.id}") + expect(doc.text).to eq "See #{commit.short_id}" + + doc = filter("See #{commit.id[0...6]}") + expect(doc.text).to eq "See #{commit.short_id}" + end + it 'links with adjacent text' do doc = filter("See (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + expect(doc.to_html).to match(/\(<a.+>#{commit.short_id}<\/a>\.\)/) end it 'ignores invalid commit IDs' do @@ -55,7 +63,7 @@ module Gitlab::Markdown allow_any_instance_of(Commit).to receive(:title).and_return(%{"></a>whatever<a title="}) doc = filter("See #{reference}") - expect(doc.text).to eq "See #{commit.id}" + expect(doc.text).to eq "See #{commit.short_id}" end it 'includes default classes' do @@ -75,6 +83,11 @@ module Gitlab::Markdown expect(link).not_to match %r(https?://) expect(link).to eq urls.namespace_project_commit_url(project.namespace, project, reference, only_path: true) end + + it 'adds to the results hash' do + result = pipeline_result("See #{reference}") + expect(result[:references][:commit]).not_to be_empty + end end context 'cross-project reference' do @@ -95,13 +108,20 @@ module Gitlab::Markdown it 'links with adjacent text' do doc = filter("Fixed (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + + exp = Regexp.escape(project2.path_with_namespace) + expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/) end it 'ignores invalid commit IDs on the referenced project' do exp = act = "Committed #{project2.path_with_namespace}##{commit.id.reverse}" expect(filter(act).to_html).to eq exp end + + it 'adds to the results hash' do + result = pipeline_result("See #{reference}") + expect(result[:references][:commit]).not_to be_empty + end end context 'when user cannot access reference' do diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb index f95b37d6954..393bf32e196 100644 --- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb @@ -34,7 +34,7 @@ module Gitlab::Markdown end it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = filter("Fixed #{reference}") expect(doc.css('a').first.attr('href')). to eq helper.url_for_issue(issue.iid, project) @@ -81,6 +81,11 @@ module Gitlab::Markdown expect(link).not_to match %r(https?://) expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true) end + + it 'adds to the results hash' do + result = pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] + end end context 'cross-project reference' do @@ -117,6 +122,11 @@ module Gitlab::Markdown expect(filter(act).to_html).to eq exp end + + it 'adds to the results hash' do + result = pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] + end end context 'when user cannot access reference' do diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb index c84e568e172..9f898837466 100644 --- a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb @@ -39,6 +39,11 @@ module Gitlab::Markdown expect(link).to eq urls.namespace_project_issues_url(project.namespace, project, label_name: label.name, only_path: true) end + it 'adds to the results hash' do + result = pipeline_result("Label #{reference}") + expect(result[:references][:label]).to eq [label] + end + describe 'label span element' do it 'includes default classes' do doc = filter("Label #{reference}") diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb index 0f66442269b..d6e745114f2 100644 --- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb @@ -69,6 +69,11 @@ module Gitlab::Markdown expect(link).not_to match %r(https?://) expect(link).to eq urls.namespace_project_merge_request_url(project.namespace, project, merge, only_path: true) end + + it 'adds to the results hash' do + result = pipeline_result("Merge #{reference}") + expect(result[:references][:merge_request]).to eq [merge] + end end context 'cross-project reference' do @@ -98,6 +103,11 @@ module Gitlab::Markdown expect(filter(act).to_html).to eq exp end + + it 'adds to the results hash' do + result = pipeline_result("Merge #{reference}") + expect(result[:references][:merge_request]).to eq [merge] + end end context 'when user cannot access reference' do diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb index 79533a90b55..a4b331157af 100644 --- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb @@ -68,6 +68,11 @@ module Gitlab::Markdown expect(link).not_to match %r(https?://) expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true) end + + it 'adds to the results hash' do + result = pipeline_result("Snippet #{reference}") + expect(result[:references][:snippet]).to eq [snippet] + end end context 'cross-project reference' do @@ -96,6 +101,11 @@ module Gitlab::Markdown expect(filter(act).to_html).to eq exp end + + it 'adds to the results hash' do + result = pipeline_result("Snippet #{reference}") + expect(result[:references][:snippet]).to eq [snippet] + end end context 'when user cannot access reference' do diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb index a5eb927072e..922502ada33 100644 --- a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb @@ -24,9 +24,29 @@ module Gitlab::Markdown end end + context 'mentioning @all' do + before do + project.team << [project.creator, :developer] + end + + it 'supports a special @all mention' do + doc = filter("Hey @all") + expect(doc.css('a').length).to eq 1 + expect(doc.css('a').first.attr('href')) + .to eq urls.namespace_project_url(project.namespace, project) + end + + it 'adds to the results hash' do + result = pipeline_result('Hey @all') + expect(result[:references][:user]).to eq [project.creator] + end + end + context 'mentioning a user' do + let(:reference) { "@#{user.username}" } + it 'links to a User' do - doc = filter("Hey @#{user.username}") + doc = filter("Hey #{reference}") expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) end @@ -45,22 +65,45 @@ module Gitlab::Markdown doc = filter("Hey @#{user.username}") expect(doc.css('a').length).to eq 1 end + + it 'adds to the results hash' do + result = pipeline_result("Hey #{reference}") + expect(result[:references][:user]).to eq [user] + end end context 'mentioning a group' do let(:group) { create(:group) } let(:user) { create(:user) } - it 'links to a Group that the current user can read' do - group.add_user(user, Gitlab::Access::DEVELOPER) + let(:reference) { "@#{group.name}" } + + context 'that the current user can read' do + before do + group.add_user(user, Gitlab::Access::DEVELOPER) + end - doc = filter("Hey @#{group.name}", current_user: user) - expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) + it 'links to the Group' do + doc = filter("Hey #{reference}", current_user: user) + expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) + end + + it 'adds to the results hash' do + result = pipeline_result("Hey #{reference}", current_user: user) + expect(result[:references][:user]).to eq group.users + end end - it 'ignores references to a Group that the current user cannot read' do - doc = filter("Hey @#{group.name}", current_user: user) - expect(doc.to_html).to eq "Hey @#{group.name}" + context 'that the current user cannot read' do + it 'ignores references to the Group' do + doc = filter("Hey #{reference}", current_user: user) + expect(doc.to_html).to eq "Hey #{reference}" + end + + it 'does not add to the results hash' do + result = pipeline_result("Hey #{reference}", current_user: user) + expect(result[:references][:user]).to eq [] + end end end @@ -70,13 +113,6 @@ module Gitlab::Markdown expect(doc.to_html).to match(/\(<a.+>@#{user.username}<\/a>\.\)/) end - it 'supports a special @all mention' do - doc = filter("Hey @all") - expect(doc.css('a').length).to eq 1 - expect(doc.css('a').first.attr('href')) - .to eq urls.namespace_project_url(project.namespace, project) - end - it 'includes default classes' do doc = filter("Hey @#{user.username}") expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member' diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 0f4ea2a24ed..9801dc16554 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -4,80 +4,6 @@ describe Gitlab::ReferenceExtractor do let(:project) { create(:project) } subject { Gitlab::ReferenceExtractor.new(project, project.creator) } - it 'extracts username references' do - subject.analyze('this contains a @user reference') - expect(subject.references[:user]).to eq([[project, 'user']]) - end - - it 'extracts issue references' do - subject.analyze('this one talks about issue #1234') - expect(subject.references[:issue]).to eq([[project, '1234']]) - end - - it 'extracts JIRA issue references' do - subject.analyze('this one talks about issue JIRA-1234') - expect(subject.references[:issue]).to eq([[project, 'JIRA-1234']]) - end - - it 'extracts merge request references' do - subject.analyze("and here's !43, a merge request") - expect(subject.references[:merge_request]).to eq([[project, '43']]) - end - - it 'extracts snippet ids' do - subject.analyze('snippets like $12 get extracted as well') - expect(subject.references[:snippet]).to eq([[project, '12']]) - end - - it 'extracts commit shas' do - subject.analyze('commit shas 98cf0ae3 are pulled out as Strings') - expect(subject.references[:commit]).to eq([[project, '98cf0ae3']]) - end - - it 'extracts commit ranges' do - subject.analyze('here you go, a commit range: 98cf0ae3...98cf0ae4') - expect(subject.references[:commit_range]).to eq([[project, '98cf0ae3...98cf0ae4']]) - end - - it 'extracts multiple references and preserves their order' do - subject.analyze('@me and @you both care about this') - expect(subject.references[:user]).to eq([ - [project, 'me'], - [project, 'you'] - ]) - end - - it 'leaves the original note unmodified' do - text = 'issue #123 is just the worst, @user' - subject.analyze(text) - expect(text).to eq('issue #123 is just the worst, @user') - end - - it 'extracts no references for <pre>..</pre> blocks' do - subject.analyze("<pre>def puts '#1 issue'\nend\n</pre>```") - expect(subject.issues).to be_blank - end - - it 'extracts no references for <code>..</code> blocks' do - subject.analyze("<code>def puts '!1 request'\nend\n</code>```") - expect(subject.merge_requests).to be_blank - end - - it 'extracts no references for code blocks with language' do - subject.analyze("this code:\n```ruby\ndef puts '#1 issue'\nend\n```") - expect(subject.issues).to be_blank - end - - it 'extracts issue references for invalid code blocks' do - subject.analyze('test: ```this one talks about issue #1234```') - expect(subject.references[:issue]).to eq([[project, '1234']]) - end - - it 'handles all possible kinds of references' do - accessors = described_class::TYPES.map { |t| "#{t}s".to_sym } - expect(subject).to respond_to(*accessors) - end - it 'accesses valid user objects' do @u_foo = create(:user, username: 'foo') @u_bar = create(:user, username: 'bar') @@ -139,12 +65,12 @@ describe Gitlab::ReferenceExtractor do earlier_commit = project.commit('master~2') subject.analyze("this references commits #{earlier_commit.sha[0..6]}...#{commit.sha[0..6]}") + extracted = subject.commit_ranges expect(extracted.size).to eq(1) - expect(extracted[0][0].sha).to eq(earlier_commit.sha) - expect(extracted[0][0].message).to eq(earlier_commit.message) - expect(extracted[0][1].sha).to eq(commit.sha) - expect(extracted[0][1].message).to eq(commit.message) + expect(extracted.first).to be_kind_of(CommitRange) + expect(extracted.first.commit_from).to eq earlier_commit + expect(extracted.first.commit_to).to eq commit end context 'with a project with an underscore' do diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb new file mode 100644 index 00000000000..31ee3e99cad --- /dev/null +++ b/spec/models/commit_range_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' + +describe CommitRange do + let(:sha_from) { 'f3f85602' } + let(:sha_to) { 'e86e1013' } + + let(:range) { described_class.new("#{sha_from}...#{sha_to}") } + let(:range2) { described_class.new("#{sha_from}..#{sha_to}") } + + it 'raises ArgumentError when given an invalid range string' do + expect { described_class.new("Foo") }.to raise_error + end + + describe '#to_s' do + it 'is correct for three-dot syntax' do + expect(range.to_s).to eq "#{sha_from[0..7]}...#{sha_to[0..7]}" + end + + it 'is correct for two-dot syntax' do + expect(range2.to_s).to eq "#{sha_from[0..7]}..#{sha_to[0..7]}" + end + end + + describe '#reference_title' do + it 'returns the correct String for three-dot ranges' do + expect(range.reference_title).to eq "Commits #{sha_from} through #{sha_to}" + end + + it 'returns the correct String for two-dot ranges' do + expect(range2.reference_title).to eq "Commits #{sha_from}^ through #{sha_to}" + end + end + + describe '#to_param' do + it 'includes the correct keys' do + expect(range.to_param.keys).to eq %i(from to) + end + + it 'includes the correct values for a three-dot range' do + expect(range.to_param).to eq({from: sha_from, to: sha_to}) + end + + it 'includes the correct values for a two-dot range' do + expect(range2.to_param).to eq({from: sha_from + '^', to: sha_to}) + end + end + + describe '#exclude_start?' do + it 'is false for three-dot ranges' do + expect(range.exclude_start?).to eq false + end + + it 'is true for two-dot ranges' do + expect(range2.exclude_start?).to eq true + end + end + + describe '#valid_commits?' do + context 'without a project' do + it 'returns nil' do + expect(range.valid_commits?).to be_nil + end + end + + it 'accepts an optional project argument' do + project1 = double('project1').as_null_object + project2 = double('project2').as_null_object + + # project1 gets assigned through the accessor, but ignored when not given + # as an argument to `valid_commits?` + expect(project1).not_to receive(:present?) + range.project = project1 + + # project2 gets passed to `valid_commits?` + expect(project2).to receive(:present?).and_return(false) + + range.valid_commits?(project2) + end + + context 'with a project' do + let(:project) { double('project', repository: double('repository')) } + + context 'with a valid repo' do + before do + expect(project).to receive(:valid_repo?).and_return(true) + range.project = project + end + + it 'is false when `sha_from` is invalid' do + expect(project.repository).to receive(:commit).with(sha_from).and_return(false) + expect(project.repository).not_to receive(:commit).with(sha_to) + expect(range).not_to be_valid_commits + end + + it 'is false when `sha_to` is invalid' do + expect(project.repository).to receive(:commit).with(sha_from).and_return(true) + expect(project.repository).to receive(:commit).with(sha_to).and_return(false) + expect(range).not_to be_valid_commits + end + + it 'is true when both `sha_from` and `sha_to` are valid' do + expect(project.repository).to receive(:commit).with(sha_from).and_return(true) + expect(project.repository).to receive(:commit).with(sha_to).and_return(true) + expect(range).to be_valid_commits + end + end + + context 'without a valid repo' do + before do + expect(project).to receive(:valid_repo?).and_return(false) + range.project = project + end + + it 'returns false' do + expect(range).not_to be_valid_commits + end + end + end + end +end diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb index fb3ff552c8d..7a784796031 100644 --- a/spec/requests/api/fork_spec.rb +++ b/spec/requests/api/fork_spec.rb @@ -50,7 +50,6 @@ describe API::API, api: true do it 'should fail if forked project exists in the user namespace' do post api("/projects/fork/#{project.id}", user) expect(response.status).to eq(409) - expect(json_response['message']['base']).to eq(['Invalid fork destination']) expect(json_response['message']['name']).to eq(['has already been taken']) expect(json_response['message']['path']).to eq(['has already been taken']) end diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index c9025bdf133..f158ac87e2b 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -27,7 +27,7 @@ describe Projects::ForkService do it "fails due to transaction failure" do @to_project = fork_project(@from_project, @to_user, false) expect(@to_project.errors).not_to be_empty - expect(@to_project.errors[:base]).to include("Fork transaction failed.") + expect(@to_project.errors[:base]).to include("Failed to fork repository") end end @@ -36,8 +36,8 @@ describe Projects::ForkService do @existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) @to_project = fork_project(@from_project, @to_user) expect(@existing_project.persisted?).to be_truthy - expect(@to_project.errors[:base]).to include("Invalid fork destination") - expect(@to_project.errors[:base]).not_to include("Fork transaction failed.") + expect(@to_project.errors[:name]).to eq(['has already been taken']) + expect(@to_project.errors[:path]).to eq(['has already been taken']) end end @@ -81,7 +81,7 @@ describe Projects::ForkService do context 'fork project for group when user not owner' do it 'group developer should fail to fork project into the group' do to_project = fork_project(@project, @developer, true, @opts) - expect(to_project.errors[:namespace]).to eq(['insufficient access rights']) + expect(to_project.errors[:namespace]).to eq(['is not valid']) end end @@ -91,7 +91,6 @@ describe Projects::ForkService do namespace: @group) to_project = fork_project(@project, @group_owner, true, @opts) expect(existing_project.persisted?).to be_truthy - expect(to_project.errors[:base]).to eq(['Invalid fork destination']) expect(to_project.errors[:name]).to eq(['has already been taken']) expect(to_project.errors[:path]).to eq(['has already been taken']) end @@ -99,10 +98,7 @@ describe Projects::ForkService do end def fork_project(from_project, user, fork_success = true, params = {}) - context = Projects::ForkService.new(from_project, user, params) - shell = double('gitlab_shell') - shell.stub(fork_repository: fork_success) - context.stub(gitlab_shell: shell) - context.execute + allow_any_instance_of(Gitlab::Shell).to receive(:fork_repository).and_return(fork_success) + Projects::ForkService.new(from_project, user, params).execute end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 53ccaa4fd67..8fe51cf4add 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -10,19 +10,13 @@ end ENV["RAILS_ENV"] ||= 'test' require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' -require 'capybara/rails' -require 'capybara/rspec' require 'webmock/rspec' require 'email_spec' require 'sidekiq/testing/inline' -require 'capybara/poltergeist' - -Capybara.javascript_driver = :poltergeist -Capybara.default_wait_time = 10 # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. -Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} +Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } WebMock.disable_net_connect!(allow_localhost: true) diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb new file mode 100644 index 00000000000..fed1ab6ee33 --- /dev/null +++ b/spec/support/capybara.rb @@ -0,0 +1,21 @@ +require 'capybara/rails' +require 'capybara/rspec' +require 'capybara/poltergeist' + +# Give CI some extra time +timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10 + +Capybara.javascript_driver = :poltergeist +Capybara.register_driver :poltergeist do |app| + Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout) +end + +Capybara.default_wait_time = timeout +Capybara.ignore_hidden_elements = true + +unless ENV['CI'] || ENV['CI_SERVER'] + require 'capybara-screenshot/rspec' + + # Keep only the screenshots generated from the last failing test suite + Capybara::Screenshot.prune_strategy = :keep_last_run +end diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index 9d0af29ff99..53fb6545553 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -53,7 +53,7 @@ def common_mentionable_setup extra_commits.each { |c| commitmap[c.short_id] = c } allow(project.repository).to receive(:commit) { |sha| commitmap[sha] } - + set_mentionable_text.call(ref_string) end end diff --git a/spec/support/reference_filter_spec_helper.rb b/spec/support/reference_filter_spec_helper.rb index bcee5715cad..41253811490 100644 --- a/spec/support/reference_filter_spec_helper.rb +++ b/spec/support/reference_filter_spec_helper.rb @@ -35,6 +35,20 @@ module ReferenceFilterSpecHelper described_class.call(html, contexts) end + # Run text through HTML::Pipeline with the current filter and return the + # result Hash + # + # body - String text to run through the pipeline + # contexts - Hash context for the filter. (default: {project: project}) + # + # Returns the Hash of the pipeline result + def pipeline_result(body, contexts = {}) + contexts.reverse_merge!(project: project) + + pipeline = HTML::Pipeline.new([described_class], contexts) + pipeline.call(body) + end + def allow_cross_reference! allow_any_instance_of(described_class). to receive(:user_can_reference_project?).and_return(true) |