Commit 278c3ba4 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce

parents 2cbdfbfd f3ffd7d0
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 7.11.0 (unreleased) v 7.11.0 (unreleased)
- Get Gitorious importer to work again.
- Fix clone URL field and X11 Primary selection (Dmitry Medvinsky) - Fix clone URL field and X11 Primary selection (Dmitry Medvinsky)
- Ignore invalid lines in .gitmodules - Ignore invalid lines in .gitmodules
- Fix "Cannot move project" error message from popping up after a successful transfer (Stan Hu) - Fix "Cannot move project" error message from popping up after a successful transfer (Stan Hu)
......
...@@ -223,14 +223,13 @@ end ...@@ -223,14 +223,13 @@ end
group :development, :test do group :development, :test do
gem 'coveralls', require: false gem 'coveralls', require: false
gem 'rubocop', '0.28.0', require: false gem 'rubocop', '0.28.0', require: false
# gem 'rails-dev-tweaks'
gem 'spinach-rails' gem 'spinach-rails'
gem "rspec-rails", '2.99' 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 "pry-rails"
gem "awesome_print" gem "awesome_print"
gem "database_cleaner" gem "database_cleaner"
gem "launchy"
gem 'factory_girl_rails' gem 'factory_girl_rails'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
......
...@@ -85,6 +85,9 @@ GEM ...@@ -85,6 +85,9 @@ GEM
rack (>= 1.0.0) rack (>= 1.0.0)
rack-test (>= 0.5.4) rack-test (>= 0.5.4)
xpath (~> 2.0) xpath (~> 2.0)
capybara-screenshot (1.0.9)
capybara (>= 1.0, < 3)
launchy
carrierwave (0.9.0) carrierwave (0.9.0)
activemodel (>= 3.2.0) activemodel (>= 3.2.0)
activesupport (>= 3.2.0) activesupport (>= 3.2.0)
...@@ -677,6 +680,7 @@ DEPENDENCIES ...@@ -677,6 +680,7 @@ DEPENDENCIES
byebug byebug
cal-heatmap-rails (~> 0.0.1) cal-heatmap-rails (~> 0.0.1)
capybara (~> 2.2.1) capybara (~> 2.2.1)
capybara-screenshot (~> 1.0.0)
carrierwave carrierwave
charlock_holmes charlock_holmes
coffee-rails coffee-rails
...@@ -726,7 +730,6 @@ DEPENDENCIES ...@@ -726,7 +730,6 @@ DEPENDENCIES
jquery-turbolinks jquery-turbolinks
jquery-ui-rails jquery-ui-rails
kaminari (~> 0.15.1) kaminari (~> 0.15.1)
launchy
letter_opener letter_opener
minitest (~> 5.3.0) minitest (~> 5.3.0)
mousetrap-rails mousetrap-rails
......
...@@ -132,10 +132,17 @@ $ -> ...@@ -132,10 +132,17 @@ $ ->
), 1 ), 1
# Initialize tooltips # Initialize tooltips
$('.has_tooltip').tooltip() $('body').tooltip({
selector: '.has_tooltip, [data-toggle="tooltip"], .page-sidebar-collapsed .nav-sidebar a'
# Bottom tooltip placement: (_, el) ->
$('.has_bottom_tooltip').tooltip(placement: 'bottom') $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 # Form submitter
$('.trigger-submit').on 'change', -> $('.trigger-submit').on 'change', ->
......
...@@ -117,7 +117,7 @@ ...@@ -117,7 +117,7 @@
color: #888; color: #888;
text-shadow: 0 1px 1px #fff; text-shadow: 0 1px 1px #fff;
} }
i[class~="fa"] { i.fa {
line-height: 14px; line-height: 14px;
} }
} }
......
...@@ -106,7 +106,6 @@ ...@@ -106,7 +106,6 @@
p > code { p > code {
font-size: inherit; font-size: inherit;
font-weight: inherit; font-weight: inherit;
color: #555;
} }
li { li {
......
...@@ -136,7 +136,7 @@ ul.notes { ...@@ -136,7 +136,7 @@ ul.notes {
display: none; display: none;
float: right; float: right;
[class~="fa"] { i.fa {
font-size: 16px; font-size: 16px;
line-height: 16px; line-height: 16px;
vertical-align: middle; vertical-align: middle;
......
...@@ -6,7 +6,7 @@ class Import::GitoriousController < Import::BaseController ...@@ -6,7 +6,7 @@ class Import::GitoriousController < Import::BaseController
def callback def callback
session[:gitorious_repos] = params[:repos] session[:gitorious_repos] = params[:repos]
redirect_to status_import_gitorious_url redirect_to status_import_gitorious_path
end end
def status def status
......
# 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
...@@ -686,6 +686,15 @@ class Project < ActiveRecord::Base ...@@ -686,6 +686,15 @@ class Project < ActiveRecord::Base
end end
def create_repository def create_repository
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
if gitlab_shell.add_repository(path_with_namespace) if gitlab_shell.add_repository(path_with_namespace)
true true
else else
...@@ -693,6 +702,7 @@ class Project < ActiveRecord::Base ...@@ -693,6 +702,7 @@ class Project < ActiveRecord::Base
false false
end end
end end
end
def repository_exists? def repository_exists?
!!repository.exists? !!repository.exists?
......
...@@ -5,6 +5,8 @@ module Projects ...@@ -5,6 +5,8 @@ module Projects
end end
def execute def execute
forked_from_project_id = params.delete(:forked_from_project_id)
@project = Project.new(params) @project = Project.new(params)
# Make sure that the user is allowed to use the specified visibility # Make sure that the user is allowed to use the specified visibility
...@@ -45,10 +47,14 @@ module Projects ...@@ -45,10 +47,14 @@ module Projects
@project.creator = current_user @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.transaction do
@project.save @project.save
unless @project.import? if @project.persisted? && !@project.import?
unless @project.create_repository unless @project.create_repository
raise 'Failed to create repository' raise 'Failed to create repository'
end end
......
module Projects module Projects
class ForkService < BaseService class ForkService < BaseService
include Gitlab::ShellAdapter
def execute def execute
@from_project = @project new_params = {
forked_from_project_id: @project.id,
project_params = { visibility_level: @project.visibility_level,
visibility_level: @from_project.visibility_level, description: @project.description,
description: @from_project.description, name: @project.name,
path: @project.path,
namespace_id: @params[:namespace].try(:id) || current_user.namespace.id
} }
project = Project.new(project_params) if @project.avatar.present? && @project.avatar.image?
project.name = @from_project.name new_params[:avatar] = @project.avatar
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
end
unless @current_user.can?(:create_projects, project.namespace)
project.errors.add(:namespace, 'insufficient access rights')
return project
end end
# If the project cannot save, we do not want to trigger the project destroy new_project = CreateService.new(current_user, new_params).execute
# 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 if new_project.persisted?
end if @project.gitlab_ci?
ForkRegistrationWorker.perform_async(@project.id, new_project.id, @current_user.private_token)
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
end end
else
project.errors.add(:base, 'Invalid fork destination')
end end
project new_project
end end
end end
end end
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
%a.twitter-share-button{ | %a.twitter-share-button{ |
href: "https://twitter.com/share", | href: "https://twitter.com/share", |
"data-url" => event.project.web_url, | "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-size" => "medium", |
"data-related" => "gitlab", | "data-related" => "gitlab", |
"data-hashtags" => "gitlab", | "data-hashtags" => "gitlab", |
......
%ul.sidebar-subnav %ul.sidebar-subnav
= nav_link(path: 'groups#edit') do = nav_link(path: 'groups#edit') do
= link_to edit_group_path(@group), title: 'Group' do = link_to edit_group_path(@group), title: 'Group', data: {placement: 'right'} do
%i.fa.fa-pencil-square-o = icon('pencil-square-o')
%span %span
Group Group
= nav_link(path: 'groups#projects') do = nav_link(path: 'groups#projects') do
= link_to projects_group_path(@group), title: 'Projects' do = link_to projects_group_path(@group), title: 'Projects', data: {placement: 'right'} do
%i.fa.fa-folder = icon('folder')
%span %span
Projects Projects
...@@ -2,48 +2,47 @@ ...@@ -2,48 +2,47 @@
.navbar-inner .navbar-inner
.container .container
%div.app_logo %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 = brand_header_logo
%h3 GitLab %h3 GitLab
%h1.title= title %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 %span.sr-only Toggle navigation
%i.fa.fa-bars = icon('bars')
.navbar-collapse.collapse .navbar-collapse.collapse
%ul.nav.navbar-nav %ul.nav.navbar-nav
%li.hidden-sm.hidden-xs %li.hidden-sm.hidden-xs
= render "layouts/search" = render 'layouts/search'
%li.visible-sm.visible-xs %li.visible-sm.visible-xs
= link_to search_path, title: "Search", class: 'has_bottom_tooltip', 'data-original-title' => 'Search area' do = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom'} do
%i.fa.fa-search = icon('search')
%li %li
= link_to help_path, title: 'Help', class: 'has_bottom_tooltip', = link_to help_path, title: 'Help', data: {toggle: 'tooltip', placement: 'bottom'} do
'data-original-title' => 'Help' do = icon('question-circle')
%i.fa.fa-question-circle
%li %li
= link_to explore_root_path, title: "Explore", class: 'has_bottom_tooltip', 'data-original-title' => 'Public area' do = link_to explore_root_path, title: 'Explore', data: {toggle: 'tooltip', placement: 'bottom'} do
%i.fa.fa-globe = icon('globe')
%li %li
= link_to user_snippets_path(current_user), title: "Your snippets", class: 'has_bottom_tooltip', 'data-original-title' => 'Your snippets' do = link_to user_snippets_path(current_user), title: 'Your snippets', data: {toggle: 'tooltip', placement: 'bottom'} do
%i.fa.fa-clipboard = icon('clipboard')
- if current_user.is_admin? - if current_user.is_admin?
%li %li
= link_to admin_root_path, title: "Admin area", class: 'has_bottom_tooltip', 'data-original-title' => 'Admin area' do = link_to admin_root_path, title: 'Admin area', data: {toggle: 'tooltip', placement: 'bottom'} do
%i.fa.fa-cogs = icon('cogs')
- if current_user.can_create_project? - if current_user.can_create_project?
%li %li
= link_to new_project_path, title: "New project", class: 'has_bottom_tooltip', 'data-original-title' => 'New project' do = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom'} do
%i.fa.fa-plus = icon('plus')
%li %li
= link_to profile_path, title: "Profile settings", class: 'has_bottom_tooltip', 'data-original-title' => 'Profile settings"' do = link_to profile_path, title: 'Profile settings', data: {toggle: 'tooltip', placement: 'bottom'} do
%i.fa.fa-user = icon('user')
%li %li
= link_to destroy_user_session_path, class: "logout", method: :delete, title: "Sign out", class: 'has_bottom_tooltip', 'data-original-title' => 'Sign out' do = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom'} do
%i.fa.fa-sign-out = icon('sign-out')
%li.hidden-xs %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' = image_tag avatar_icon(current_user.email, 60), alt: 'User activity'
= render 'shared/outdated_browser' = render 'shared/outdated_browser'
...@@ -15,7 +15,3 @@ ...@@ -15,7 +15,3 @@
= yield = yield
= yield :embedded_scripts = yield :embedded_scripts
:coffeescript
$('.page-sidebar-collapsed .nav-sidebar a').tooltip placement: "right"
...@@ -5,60 +5,60 @@ ...@@ -5,60 +5,60 @@
%span %span
Overview Overview
= nav_link(controller: :projects) do = 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') = icon('cube fw')
%span %span
Projects Projects
= nav_link(controller: :users) do = 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') = icon('user fw')
%span %span
Users Users
= nav_link(controller: :groups) do = 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') = icon('group fw')
%span %span
Groups Groups
= nav_link(controller: :deploy_keys) do = 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') = icon('key fw')
%span %span
Deploy Keys Deploy Keys
= nav_link(controller: :logs) do = 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') = icon('file-text fw')
%span %span
Logs Logs
= nav_link(controller: :broadcast_messages) do = 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') = icon('bullhorn fw')
%span %span
Messages Messages
= nav_link(controller: :hooks) do = 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') = icon('external-link fw')
%span %span
Hooks Hooks
= nav_link(controller: :background_jobs) do = 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') = icon('cog fw')
%span %span
Background Jobs Background Jobs
= nav_link(controller: :applications) do = 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') = icon('cloud fw')
%span %span
Applications Applications
= nav_link(controller: :services) do = 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') = icon('copy fw')
%span %span
Service Templates Service Templates
= nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do = 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') = icon('cogs fw')
%span %span
Settings Settings
%ul.nav.nav-sidebar %ul.nav.nav-sidebar
= nav_link(path: 'dashboard#show', html_options: {class: 'home'}) do = 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') = icon('dashboard fw')
%span %span
Your Projects Your Projects
= nav_link(path: 'projects#starred') do = 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') = icon('star fw')
%span %span
Starred Projects Starred Projects
= nav_link(controller: :groups) do = 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') = icon('group fw')
%span %span
Groups Groups
= nav_link(controller: :milestones) do = 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') = icon('clock-o fw')
%span %span
Milestones Milestones
= nav_link(path: 'dashboard#issues') do = 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') = icon('exclamation-circle fw')
%span %span
Issues Issues
%span.count= current_user.assigned_issues.opened.count %span.count= current_user.assigned_issues.opened.count
= nav_link(path: 'dashboard#merge_requests') do = 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') = icon('tasks fw')
%span %span
Merge Requests Merge Requests
%span.count= current_user.assigned_merge_requests.opened.count %span.count= current_user.assigned_merge_requests.opened.count
= nav_link(controller: :help) do = 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') = icon('question-circle fw')
%span %span
Help Help
%ul.nav.nav-sidebar %ul.nav.nav-sidebar
= nav_link(path: 'projects#trending') do = 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') = icon('comments fw')
%span Trending Projects %span Trending Projects
= nav_link(path: 'projects#starred') do = 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') = icon('star fw')
%span Most Starred Projects %span Most-starred Projects
= nav_link(path: 'projects#index') do = 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') = icon('bookmark fw')
%span All Projects %span All Projects
= nav_link(controller: :groups) do = 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') = icon('group fw')
%span All Groups %span All Groups
%ul.nav.nav-sidebar %ul.nav.nav-sidebar
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do = 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') = icon('dashboard fw')
%span %span
Activity Activity
- if current_user - if current_user
= nav_link(controller: [:group, :milestones]) do = 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') = icon('clock-o fw')
%span %span
Milestones Milestones
= nav_link(path: 'groups#issues') do = 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') = icon('exclamation-circle fw')
%span %span
Issues Issues
- if current_user - if current_user
%span.count= Issue.opened.of_group(@group).count %span.count= Issue.opened.of_group(@group).count
= nav_link(path: 'groups#merge_requests') do = 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') = icon('tasks fw')
%span %span
Merge Requests Merge Requests
- if current_user - if current_user
%span.count= MergeRequest.opened.of_group(@group).count %span.count= MergeRequest.opened.of_group(@group).count
= nav_link(controller: [:group_members]) do = 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') = icon('users fw')
%span %span
Members Members
- if can?(current_user, :admin_group, @group) - if can?(current_user, :admin_group, @group)
= nav_link(html_options: { class: "#{"active" if group_settings_page?} separate-item" }) do = 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') = icon ('cogs fw')
%span %span
Settings Settings
......
%ul.nav.nav-sidebar %ul.nav.nav-sidebar
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = 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') = icon('user fw')
%span %span
Profile Profile
= nav_link(controller: :accounts) do = 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') = icon('gear fw')
%span %span
Account Account
= nav_link(path: ['profiles#applications', 'applications#edit', 'applications#show', 'applications#new']) do = 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') = icon('cloud fw')
%span %span
Applications Applications
= nav_link(controller: :emails) do = 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') = icon('envelope-o fw')
%span %span
Emails Emails
%span.count= current_user.emails.count + 1 %span.count= current_user.emails.count + 1
- unless current_user.ldap_user? - unless current_user.ldap_user?
= nav_link(controller: :passwords) do = 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') = icon('lock fw')
%span %span
Password Password
= nav_link(controller: :notifications) do = 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') = icon('inbox fw')
%span %span
Notifications Notifications
= nav_link(controller: :keys) do = 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') = icon('key fw')
%span %span
SSH Keys SSH Keys
%span.count= current_user.keys.count %span.count= current_user.keys.count
= nav_link(path: 'profiles#design') do = 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') = icon('image fw')
%span %span
Design Design
= nav_link(path: 'profiles#history') do = 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') = icon('history fw')
%span %span
History History
%ul.project-navigation.nav.nav-sidebar %ul.project-navigation.nav.nav-sidebar
- if @project_settings_nav - if @project_settings_nav
= nav_link do = 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') = icon('caret-square-o-left fw')
%span %span
Back to project Back to project
...@@ -11,49 +11,49 @@ ...@@ -11,49 +11,49 @@
= render 'projects/settings_nav' = render 'projects/settings_nav'
- else - else
= nav_link(path: 'projects#show', html_options: {class: "home"}) do = nav_link(path: 'projects#show', html_options: {class: 'home'}) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do = link_to project_path(@project), title: 'Project', class: 'shortcuts-project', data: {placement: 'right'} do
= icon('dashboard fw') = icon('dashboard fw')
%span %span
Project Project
- if project_nav_tab? :files - if project_nav_tab? :files
= nav_link(controller: %w(tree blob blame edit_tree new_tree)) do = 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') = icon('files-o fw')
%span %span
Files Files
- if project_nav_tab? :commits - if project_nav_tab? :commits
= nav_link(controller: %w(commit commits compare repositories tags branches)) do = nav_link(controller: %w(commit commits compare repositories tags branches)) 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') = icon('history fw')
%span %span
Commits Commits
- if project_nav_tab? :network - if project_nav_tab? :network
= nav_link(controller: %w(network)) do = 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') = icon('code-fork fw')
%span %span
Network Network
- if project_nav_tab? :graphs - if project_nav_tab? :graphs
= nav_link(controller: %w(graphs)) do = 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') = icon('area-chart fw')
%span %span
Graphs Graphs
- if project_nav_tab? :milestones - if project_nav_tab? :milestones
= nav_link(controller: :milestones) do = 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') = icon('clock-o fw')
%span %span
Milestones Milestones
- if project_nav_tab? :issues - if project_nav_tab? :issues
= nav_link(controller: :issues) do = 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') = icon('exclamation-circle fw')
%span %span
Issues Issues
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
- if project_nav_tab? :merge_requests - if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do = 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') = icon('tasks fw')
%span %span
Merge Requests Merge Requests
...@@ -70,28 +70,28 @@ ...@@ -70,28 +70,28 @@
- if project_nav_tab? :labels - if project_nav_tab? :labels
= nav_link(controller: :labels) do = 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') = icon('tags fw')
%span %span
Labels Labels
- if project_nav_tab? :wiki - if project_nav_tab? :wiki
= nav_link(controller: :wikis) do = 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') = icon('book fw')
%span %span
Wiki Wiki
- if project_nav_tab? :snippets - if project_nav_tab? :snippets
= nav_link(controller: :snippets) do = 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') = icon('file-text-o fw')
%span %span
Snippets Snippets
- if project_nav_tab? :settings - if project_nav_tab? :settings
= nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do = 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') = icon('cogs fw')
%span %span
Settings Settings
%ul.nav.nav-sidebar %ul.nav.nav-sidebar
= nav_link(path: user_snippets_path(current_user), html_options: {class: 'home'}) do = 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') = icon('dashboard fw')
%span %span
Your Snippets Your Snippets
= nav_link(path: snippets_path) do = 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') = icon('globe fw')
%span %span
Discover Snippets Discover Snippets
// Remove body class for any previous theme, re-add current one // 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 %>') $('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'})
%ul.project-settings-nav.sidebar-subnav %ul.project-settings-nav.sidebar-subnav
= nav_link(path: 'projects#edit') do = nav_link(path: 'projects#edit') do
= link_to edit_project_path(@project), title: 'Project', class: "stat-tab tab " do = link_to edit_project_path(@project), title: 'Project', class: 'stat-tab tab', data: {placement: 'right'} do
%i.fa.fa-pencil-square-o = icon('pencil-square-o')
%span %span
Project Project
= nav_link(controller: [:project_members, :teams]) do = nav_link(controller: [:project_members, :teams]) do
= link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: "team-tab tab" do = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab', data: {placement: 'right'} do
%i.fa.fa-users = icon('users')
%span %span
Members Members
= nav_link(controller: :deploy_keys) do = nav_link(controller: :deploy_keys) do
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys', data: {placement: 'right'} do
%i.fa.fa-key = icon('key')
%span %span
Deploy Keys Deploy Keys
= nav_link(controller: :hooks) do = nav_link(controller: :hooks) do
= link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks' do = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks', data: {placement: 'right'} do
%i.fa.fa-link = icon('link')
%span %span
Web Hooks Web Hooks
= nav_link(controller: :services) do = nav_link(controller: :services) do
= link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services', data: {placement: 'right'} do
%i.fa.fa-cogs = icon('cogs')
%span %span
Services Services
= nav_link(controller: :protected_branches) do = nav_link(controller: :protected_branches) do
= link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches', data: {placement: 'right'} do
%i.fa.fa-lock = icon('lock')
%span %span
Protected branches Protected branches
...@@ -4,8 +4,10 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps ...@@ -4,8 +4,10 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
include SharedProject include SharedProject
step 'I click "New project" link' do step 'I click "New project" link' do
within('.content') do
click_link "New project" click_link "New project"
end end
end
step 'I see "New project" page' do step 'I see "New project" page' do
page.should have_content("Project path") page.should have_content("Project path")
......
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
require 'database_cleaner'
DatabaseCleaner.strategy = :truncation
Spinach.hooks.before_scenario do
DatabaseCleaner.start
end
Spinach.hooks.after_scenario do
DatabaseCleaner.clean
end
...@@ -11,40 +11,18 @@ ENV['RAILS_ENV'] = 'test' ...@@ -11,40 +11,18 @@ ENV['RAILS_ENV'] = 'test'
require './config/environment' require './config/environment'
require 'rspec' require 'rspec'
require 'rspec/expectations' require 'rspec/expectations'
require 'database_cleaner'
require 'spinach/capybara'
require 'sidekiq/testing/inline' require 'sidekiq/testing/inline'
require_relative 'capybara'
require_relative 'db_cleaner'
%w(select2_helper test_env repo_helpers).each do |f| %w(select2_helper test_env repo_helpers).each do |f|
require Rails.root.join('spec', 'support', f) require Rails.root.join('spec', 'support', f)
end 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! 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 Spinach.hooks.before_run do
include RSpec::Mocks::ExampleMethods include RSpec::Mocks::ExampleMethods
......
...@@ -14,7 +14,7 @@ module Gitlab ...@@ -14,7 +14,7 @@ module Gitlab
end end
def repos def repos
@repos ||= repo_names.map { |full_name| Repository.new(full_name) } @repos ||= repo_names.map { |full_name| GitoriousImport::Repository.new(full_name) }
end end
def repo(id) def repo(id)
......
...@@ -32,11 +32,8 @@ module Gitlab ...@@ -32,11 +32,8 @@ module Gitlab
# Pattern used to extract commit range references from text # 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. # 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 def call
replace_text_nodes_matching(COMMIT_RANGE_PATTERN) do |content| replace_text_nodes_matching(COMMIT_RANGE_PATTERN) do |content|
...@@ -53,52 +50,34 @@ module Gitlab ...@@ -53,52 +50,34 @@ module Gitlab
# links have `gfm` and `gfm-commit_range` class names attached for # links have `gfm` and `gfm-commit_range` class names attached for
# styling. # styling.
def commit_range_link_filter(text) 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) 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, range)
url = url_for_commit_range(project, from_id, to_id)
title = "Commits #{from_id} through #{to_id}" title = range.reference_title
klass = reference_class(:commit_range) klass = reference_class(:commit_range)
project_ref += '@' if project_ref project_ref += '@' if project_ref
%(<a href="#{url}" %(<a href="#{url}"
title="#{title}" title="#{title}"
class="#{klass}">#{project_ref}#{commit_range}</a>) class="#{klass}">#{project_ref}#{range}</a>)
else else
match match
end end
end end
end end
def split_commit_range(range) def url_for_commit_range(project, 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)
h = Rails.application.routes.url_helpers h = Rails.application.routes.url_helpers
h.namespace_project_compare_url(project.namespace, project, h.namespace_project_compare_url(project.namespace, project,
from: from_id, to: to_id, range.to_param.merge(only_path: context[:only_path]))
only_path: context[:only_path])
end end
end end
end end
......
...@@ -48,6 +48,8 @@ module Gitlab ...@@ -48,6 +48,8 @@ module Gitlab
project = self.project_from_ref(project_ref) project = self.project_from_ref(project_ref)
if commit = commit_from_ref(project, commit_ref) if commit = commit_from_ref(project, commit_ref)
push_result(:commit, commit)
url = url_for_commit(project, commit) url = url_for_commit(project, commit)
title = escape_once(commit.link_title) title = escape_once(commit.link_title)
...@@ -57,7 +59,7 @@ module Gitlab ...@@ -57,7 +59,7 @@ module Gitlab
%(<a href="#{url}" %(<a href="#{url}"
title="#{title}" title="#{title}"
class="#{klass}">#{project_ref}#{commit_ref}</a>) class="#{klass}">#{project_ref}#{commit.short_id}</a>)
else else
match match
end end
......
...@@ -48,6 +48,9 @@ module Gitlab ...@@ -48,6 +48,9 @@ module Gitlab
project = self.project_from_ref(project_ref) project = self.project_from_ref(project_ref)
if project && project.issue_exists?(issue) 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]) url = url_for_issue(issue, project, only_path: context[:only_path])
title = escape_once("Issue: #{title_for_issue(issue, project)}") title = escape_once("Issue: #{title_for_issue(issue, project)}")
......
...@@ -52,11 +52,13 @@ module Gitlab ...@@ -52,11 +52,13 @@ module Gitlab
params = label_params(id, name) params = label_params(id, name)
if label = project.labels.find_by(params) 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) 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 else
match match
end end
......
...@@ -48,6 +48,8 @@ module Gitlab ...@@ -48,6 +48,8 @@ module Gitlab
project = self.project_from_ref(project_ref) project = self.project_from_ref(project_ref)
if project && merge_request = project.merge_requests.find_by(iid: id) 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}") title = escape_once("Merge Request: #{merge_request.title}")
klass = reference_class(:merge_request) klass = reference_class(:merge_request)
......
...@@ -12,7 +12,15 @@ module Gitlab ...@@ -12,7 +12,15 @@ module Gitlab
# :reference_class - Custom CSS class added to reference links. # :reference_class - Custom CSS class added to reference links.
# :only_path - Generate path-only links. # :only_path - Generate path-only links.
# #
# Results:
# :references - A Hash of references that were found and replaced.
class ReferenceFilter < HTML::Pipeline::Filter class ReferenceFilter < HTML::Pipeline::Filter
def initialize(*args)
super
result[:references] = Hash.new { |hash, type| hash[type] = [] }
end
def escape_once(html) def escape_once(html)
ERB::Util.html_escape_once(html) ERB::Util.html_escape_once(html)
end end
...@@ -29,6 +37,16 @@ module Gitlab ...@@ -29,6 +37,16 @@ module Gitlab
context[:project] context[:project]
end 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) def reference_class(type)
"gfm gfm-#{type} #{context[:reference_class]}".strip "gfm gfm-#{type} #{context[:reference_class]}".strip
end end
......
...@@ -48,6 +48,8 @@ module Gitlab ...@@ -48,6 +48,8 @@ module Gitlab
project = self.project_from_ref(project_ref) project = self.project_from_ref(project_ref)
if project && snippet = project.snippets.find_by(id: id) if project && snippet = project.snippets.find_by(id: id)
push_result(:snippet, snippet)
title = escape_once("Snippet: #{snippet.title}") title = escape_once("Snippet: #{snippet.title}")
klass = reference_class(:snippet) klass = reference_class(:snippet)
......
...@@ -38,27 +38,11 @@ module Gitlab ...@@ -38,27 +38,11 @@ module Gitlab
# Returns a String with `@user` references replaced with links. All links # Returns a String with `@user` references replaced with links. All links
# have `gfm` and `gfm-project_member` class names attached for styling. # have `gfm` and `gfm-project_member` class names attached for styling.
def user_link_filter(text) def user_link_filter(text)
project = context[:project] self.class.references_in(text) do |match, username|
if username == 'all'
self.class.references_in(text) do |match, user| link_to_all
klass = reference_class(:project_member) elsif namespace = Namespace.find_by(path: username)
link_to_namespace(namespace) || match
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
else else
match match
end end
...@@ -71,17 +55,46 @@ module Gitlab ...@@ -71,17 +55,46 @@ module Gitlab
Rails.application.routes.url_helpers Rails.application.routes.url_helpers
end end
def group_url(*args) def link_class
urls.group_url(*args) reference_class(:project_member)
end end
def user_url(*args) def link_to_all
urls.user_url(*args) project = context[:project]
end
# FIXME (rspeicher): Law of Demeter
push_result(:user, *project.team.members.flatten)
def link_to_all(project) url = urls.namespace_project_url(project.namespace, project,
urls.namespace_project_url(project.namespace, project,
only_path: context[:only_path]) 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 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_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 end
def user_can_reference_group?(group) def user_can_reference_group?(group)
......
...@@ -8,151 +8,70 @@ module Gitlab ...@@ -8,151 +8,70 @@ module Gitlab
@current_user = current_user @current_user = current_user
end end
def can?(user, action, subject)
Ability.abilities.allowed?(user, action, subject)
end
def analyze(text) def analyze(text)
text = text.dup @_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)
end end
# Given a valid project, resolve the extracted identifiers of the requested type to
# model objects.
def users def users
references[:user].uniq.map do |project, identifier| result = pipeline_result(:user)
if identifier == "all" result.uniq
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
end end
def labels def labels
references[:label].uniq.map do |project, identifier| result = pipeline_result(:label)
project.labels.where(id: identifier).first result.uniq
end.compact.uniq
end end
def issues def issues
references[:issue].uniq.map do |project, identifier| # TODO (rspeicher): What about external issues?
if project.default_issues_tracker?
project.issues.where(iid: identifier).first result = pipeline_result(:issue)
end result.uniq
end.compact.uniq
end end
def merge_requests def merge_requests
references[:merge_request].uniq.map do |project, identifier| result = pipeline_result(:merge_request)
project.merge_requests.where(iid: identifier).first result.uniq
end.compact.uniq
end end
def snippets def snippets
references[:snippet].uniq.map do |project, identifier| result = pipeline_result(:snippet)
project.snippets.where(id: identifier).first result.uniq
end.compact.uniq
end end
def commits def commits
references[:commit].uniq.map do |project, identifier| result = pipeline_result(:commit)
repo = project.repository result.uniq
repo.commit(identifier) if repo
end.compact.uniq
end end
def commit_ranges def commit_ranges
references[:commit_range].uniq.map do |project, identifier| result = pipeline_result(:commit_range)
repo = project.repository result.uniq
if repo
from_id, to_id = identifier.split(/\.{2,3}/, 2)
[repo.commit(from_id), repo.commit(to_id)]
end
end.compact.uniq
end end
private private
NAME_STR = Gitlab::Regex::NAMESPACE_REGEX_STR # Instantiate and call HTML::Pipeline with a single reference filter type,
PROJ_STR = "(?<project>#{NAME_STR}/#{NAME_STR})" # returning the result
#
REFERENCE_PATTERN = %r{ # filter_type - Symbol reference type (e.g., :commit, :issue, etc.)
(?<prefix>\W)? # Prefix #
( # Reference # Returns the results Array for the requested filter type
@(?<user>#{NAME_STR}) # User name def pipeline_result(filter_type)
|~(?<label>\d+) # Label ID klass = filter_type.to_s.camelize + 'ReferenceFilter'
|(?<issue>([A-Z\-]+-)\d+) # JIRA Issue ID filter = "Gitlab::Markdown::#{klass}".constantize
|#{PROJ_STR}?\#(?<issue>([a-zA-Z\-]+-)?\d+) # Issue ID
|#{PROJ_STR}?!(?<merge_request>\d+) # MR ID context = {
|\$(?<snippet>\d+) # Snippet ID project: project,
|(#{PROJ_STR}@)?(?<commit_range>[\h]{6,40}\.{2,3}[\h]{6,40}) # Commit range current_user: current_user,
|(#{PROJ_STR}@)?(?<commit>[\h]{6,40}) # Commit ID # We don't actually care about the links generated
) only_path: true
(?<suffix>\W)? # Suffix }
}x.freeze
pipeline = HTML::Pipeline.new([filter], context)
TYPES = %i(user issue label merge_request snippet commit commit_range).freeze result = pipeline.call(@_text)
def parse_references(text, project = @project) result[:references][filter_type]
# 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 &amp;
def html_entity?(prefix, suffix)
prefix && suffix && prefix[0] == '&' && suffix[-1] == ';'
end
def reference_link(type, identifier, project, _)
references[type] << [project, identifier]
end end
end end
end end
...@@ -7,33 +7,44 @@ module Gitlab ...@@ -7,33 +7,44 @@ module Gitlab
COLOR = 5 unless const_defined?(:COLOR) COLOR = 5 unless const_defined?(:COLOR)
BLUE = 6 unless const_defined?(:BLUE) BLUE = 6 unless const_defined?(:BLUE)
def self.css_class_by_id(id) def self.classes
themes = { @classes ||= {
BASIC => "ui_basic", BASIC => 'ui_basic',
MARS => "ui_mars", MARS => 'ui_mars',
MODERN => "ui_modern", MODERN => 'ui_modern',
GRAY => "ui_gray", GRAY => 'ui_gray',
COLOR => "ui_color", COLOR => 'ui_color',
BLUE => "ui_blue" BLUE => 'ui_blue'
} }
end
def self.css_class_by_id(id)
id ||= Gitlab.config.gitlab.default_theme id ||= Gitlab.config.gitlab.default_theme
classes[id]
themes[id]
end end
def self.type_css_class_by_id(id) def self.types
types = { @types ||= {
BASIC => 'light_theme', BASIC => 'light_theme',
MARS => 'dark_theme', MARS => 'dark_theme',
MODERN => 'dark_theme', MODERN => 'dark_theme',
GRAY => '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 id ||= Gitlab.config.gitlab.default_theme
types[id] types[id]
end 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
end end
...@@ -42,13 +42,17 @@ module Gitlab::Markdown ...@@ -42,13 +42,17 @@ module Gitlab::Markdown
reference = "#{commit1.short_id}...#{commit2.id}" reference = "#{commit1.short_id}...#{commit2.id}"
reference2 = "#{commit1.id}...#{commit2.short_id}" reference2 = "#{commit1.id}...#{commit2.short_id}"
expect(filter("See #{reference}").css('a').first.text).to eq reference exp = commit1.short_id + '...' + commit2.short_id
expect(filter("See #{reference2}").css('a').first.text).to eq reference2
expect(filter("See #{reference}").css('a').first.text).to eq exp
expect(filter("See #{reference2}").css('a').first.text).to eq exp
end end
it 'links with adjacent text' do it 'links with adjacent text' do
doc = filter("See (#{reference}.)") 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 end
it 'ignores invalid commit IDs' do it 'ignores invalid commit IDs' do
...@@ -81,6 +85,11 @@ module Gitlab::Markdown ...@@ -81,6 +85,11 @@ module Gitlab::Markdown
expect(link).not_to match %r(https?://) 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) expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true)
end end
it 'adds to the results hash' do
result = pipeline_result("See #{reference}")
expect(result[:references][:commit_range]).not_to be_empty
end
end end
context 'cross-project reference' do context 'cross-project reference' do
...@@ -102,7 +111,9 @@ module Gitlab::Markdown ...@@ -102,7 +111,9 @@ module Gitlab::Markdown
it 'links with adjacent text' do it 'links with adjacent text' do
doc = filter("Fixed (#{reference}.)") 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 end
it 'ignores invalid commit IDs on the referenced project' do it 'ignores invalid commit IDs on the referenced project' do
...@@ -112,6 +123,11 @@ module Gitlab::Markdown ...@@ -112,6 +123,11 @@ module Gitlab::Markdown
exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id}...#{commit2.id.reverse}" exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id}...#{commit2.id.reverse}"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
it 'adds to the results hash' do
result = pipeline_result("See #{reference}")
expect(result[:references][:commit_range]).not_to be_empty
end
end end
context 'when user cannot access reference' do context 'when user cannot access reference' do
......
...@@ -27,15 +27,23 @@ module Gitlab::Markdown ...@@ -27,15 +27,23 @@ module Gitlab::Markdown
it "links to a valid reference of #{size} characters" do it "links to a valid reference of #{size} characters" do
doc = filter("See #{reference[0...size]}") 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')). expect(doc.css('a').first.attr('href')).
to eq urls.namespace_project_commit_url(project.namespace, project, reference) to eq urls.namespace_project_commit_url(project.namespace, project, reference)
end end
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 it 'links with adjacent text' do
doc = filter("See (#{reference}.)") 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 end
it 'ignores invalid commit IDs' do it 'ignores invalid commit IDs' do
...@@ -55,7 +63,7 @@ module Gitlab::Markdown ...@@ -55,7 +63,7 @@ module Gitlab::Markdown
allow_any_instance_of(Commit).to receive(:title).and_return(%{"></a>whatever<a title="}) allow_any_instance_of(Commit).to receive(:title).and_return(%{"></a>whatever<a title="})
doc = filter("See #{reference}") doc = filter("See #{reference}")
expect(doc.text).to eq "See #{commit.id}" expect(doc.text).to eq "See #{commit.short_id}"
end end
it 'includes default classes' do it 'includes default classes' do
...@@ -75,6 +83,11 @@ module Gitlab::Markdown ...@@ -75,6 +83,11 @@ module Gitlab::Markdown
expect(link).not_to match %r(https?://) expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_commit_url(project.namespace, project, reference, only_path: true) expect(link).to eq urls.namespace_project_commit_url(project.namespace, project, reference, only_path: true)
end end
it 'adds to the results hash' do
result = pipeline_result("See #{reference}")
expect(result[:references][:commit]).not_to be_empty
end
end end
context 'cross-project reference' do context 'cross-project reference' do
...@@ -95,13 +108,20 @@ module Gitlab::Markdown ...@@ -95,13 +108,20 @@ module Gitlab::Markdown
it 'links with adjacent text' do it 'links with adjacent text' do
doc = filter("Fixed (#{reference}.)") 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 end
it 'ignores invalid commit IDs on the referenced project' do it 'ignores invalid commit IDs on the referenced project' do
exp = act = "Committed #{project2.path_with_namespace}##{commit.id.reverse}" exp = act = "Committed #{project2.path_with_namespace}##{commit.id.reverse}"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
it 'adds to the results hash' do
result = pipeline_result("See #{reference}")
expect(result[:references][:commit]).not_to be_empty
end
end end
context 'when user cannot access reference' do context 'when user cannot access reference' do
......
...@@ -34,7 +34,7 @@ module Gitlab::Markdown ...@@ -34,7 +34,7 @@ module Gitlab::Markdown
end end
it 'links to a valid reference' do it 'links to a valid reference' do
doc = filter("See #{reference}") doc = filter("Fixed #{reference}")
expect(doc.css('a').first.attr('href')). expect(doc.css('a').first.attr('href')).
to eq helper.url_for_issue(issue.iid, project) to eq helper.url_for_issue(issue.iid, project)
...@@ -81,6 +81,11 @@ module Gitlab::Markdown ...@@ -81,6 +81,11 @@ module Gitlab::Markdown
expect(link).not_to match %r(https?://) expect(link).not_to match %r(https?://)
expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true) expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true)
end end
it 'adds to the results hash' do
result = pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue]
end
end end
context 'cross-project reference' do context 'cross-project reference' do
...@@ -117,6 +122,11 @@ module Gitlab::Markdown ...@@ -117,6 +122,11 @@ module Gitlab::Markdown
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
it 'adds to the results hash' do
result = pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue]
end
end end
context 'when user cannot access reference' do context 'when user cannot access reference' do
......
...@@ -39,6 +39,11 @@ module Gitlab::Markdown ...@@ -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) expect(link).to eq urls.namespace_project_issues_url(project.namespace, project, label_name: label.name, only_path: true)
end 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 describe 'label span element' do
it 'includes default classes' do it 'includes default classes' do
doc = filter("Label #{reference}") doc = filter("Label #{reference}")
......
...@@ -69,6 +69,11 @@ module Gitlab::Markdown ...@@ -69,6 +69,11 @@ module Gitlab::Markdown
expect(link).not_to match %r(https?://) expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_merge_request_url(project.namespace, project, merge, only_path: true) expect(link).to eq urls.namespace_project_merge_request_url(project.namespace, project, merge, only_path: true)
end end
it 'adds to the results hash' do
result = pipeline_result("Merge #{reference}")
expect(result[:references][:merge_request]).to eq [merge]
end
end end
context 'cross-project reference' do context 'cross-project reference' do
...@@ -98,6 +103,11 @@ module Gitlab::Markdown ...@@ -98,6 +103,11 @@ module Gitlab::Markdown
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
it 'adds to the results hash' do
result = pipeline_result("Merge #{reference}")
expect(result[:references][:merge_request]).to eq [merge]
end
end end
context 'when user cannot access reference' do context 'when user cannot access reference' do
......
...@@ -68,6 +68,11 @@ module Gitlab::Markdown ...@@ -68,6 +68,11 @@ module Gitlab::Markdown
expect(link).not_to match %r(https?://) expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true) expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true)
end end
it 'adds to the results hash' do
result = pipeline_result("Snippet #{reference}")
expect(result[:references][:snippet]).to eq [snippet]
end
end end
context 'cross-project reference' do context 'cross-project reference' do
...@@ -96,6 +101,11 @@ module Gitlab::Markdown ...@@ -96,6 +101,11 @@ module Gitlab::Markdown
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
it 'adds to the results hash' do
result = pipeline_result("Snippet #{reference}")
expect(result[:references][:snippet]).to eq [snippet]
end
end end
context 'when user cannot access reference' do context 'when user cannot access reference' do
......
...@@ -24,9 +24,29 @@ module Gitlab::Markdown ...@@ -24,9 +24,29 @@ module Gitlab::Markdown
end end
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 context 'mentioning a user' do
let(:reference) { "@#{user.username}" }
it 'links to a User' do 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) expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
end end
...@@ -45,22 +65,45 @@ module Gitlab::Markdown ...@@ -45,22 +65,45 @@ module Gitlab::Markdown
doc = filter("Hey @#{user.username}") doc = filter("Hey @#{user.username}")
expect(doc.css('a').length).to eq 1 expect(doc.css('a').length).to eq 1
end end
it 'adds to the results hash' do
result = pipeline_result("Hey #{reference}")
expect(result[:references][:user]).to eq [user]
end
end end
context 'mentioning a group' do context 'mentioning a group' do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:user) { create(:user) } let(:user) { create(:user) }
it 'links to a Group that the current user can read' do let(:reference) { "@#{group.name}" }
context 'that the current user can read' do
before do
group.add_user(user, Gitlab::Access::DEVELOPER) group.add_user(user, Gitlab::Access::DEVELOPER)
end
doc = filter("Hey @#{group.name}", current_user: user) 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) expect(doc.css('a').first.attr('href')).to eq urls.group_url(group)
end end
it 'ignores references to a Group that the current user cannot read' do it 'adds to the results hash' do
doc = filter("Hey @#{group.name}", current_user: user) result = pipeline_result("Hey #{reference}", current_user: user)
expect(doc.to_html).to eq "Hey @#{group.name}" expect(result[:references][:user]).to eq group.users
end
end
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
end end
...@@ -70,13 +113,6 @@ module Gitlab::Markdown ...@@ -70,13 +113,6 @@ module Gitlab::Markdown
expect(doc.to_html).to match(/\(<a.+>@#{user.username}<\/a>\.\)/) expect(doc.to_html).to match(/\(<a.+>@#{user.username}<\/a>\.\)/)
end 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 it 'includes default classes' do
doc = filter("Hey @#{user.username}") doc = filter("Hey @#{user.username}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member' expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member'
......
...@@ -4,80 +4,6 @@ describe Gitlab::ReferenceExtractor do ...@@ -4,80 +4,6 @@ describe Gitlab::ReferenceExtractor do
let(:project) { create(:project) } let(:project) { create(:project) }
subject { Gitlab::ReferenceExtractor.new(project, project.creator) } 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 it 'accesses valid user objects' do
@u_foo = create(:user, username: 'foo') @u_foo = create(:user, username: 'foo')
@u_bar = create(:user, username: 'bar') @u_bar = create(:user, username: 'bar')
...@@ -139,12 +65,12 @@ describe Gitlab::ReferenceExtractor do ...@@ -139,12 +65,12 @@ describe Gitlab::ReferenceExtractor do
earlier_commit = project.commit('master~2') earlier_commit = project.commit('master~2')
subject.analyze("this references commits #{earlier_commit.sha[0..6]}...#{commit.sha[0..6]}") subject.analyze("this references commits #{earlier_commit.sha[0..6]}...#{commit.sha[0..6]}")
extracted = subject.commit_ranges extracted = subject.commit_ranges
expect(extracted.size).to eq(1) expect(extracted.size).to eq(1)
expect(extracted[0][0].sha).to eq(earlier_commit.sha) expect(extracted.first).to be_kind_of(CommitRange)
expect(extracted[0][0].message).to eq(earlier_commit.message) expect(extracted.first.commit_from).to eq earlier_commit
expect(extracted[0][1].sha).to eq(commit.sha) expect(extracted.first.commit_to).to eq commit
expect(extracted[0][1].message).to eq(commit.message)
end end
context 'with a project with an underscore' do context 'with a project with an underscore' do
......
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
...@@ -50,7 +50,6 @@ describe API::API, api: true do ...@@ -50,7 +50,6 @@ describe API::API, api: true do
it 'should fail if forked project exists in the user namespace' do it 'should fail if forked project exists in the user namespace' do
post api("/projects/fork/#{project.id}", user) post api("/projects/fork/#{project.id}", user)
expect(response.status).to eq(409) 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']['name']).to eq(['has already been taken'])
expect(json_response['message']['path']).to eq(['has already been taken']) expect(json_response['message']['path']).to eq(['has already been taken'])
end end
......
...@@ -27,7 +27,7 @@ describe Projects::ForkService do ...@@ -27,7 +27,7 @@ describe Projects::ForkService do
it "fails due to transaction failure" do it "fails due to transaction failure" do
@to_project = fork_project(@from_project, @to_user, false) @to_project = fork_project(@from_project, @to_user, false)
expect(@to_project.errors).not_to be_empty 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
end end
...@@ -36,8 +36,8 @@ describe Projects::ForkService do ...@@ -36,8 +36,8 @@ describe Projects::ForkService do
@existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) @existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
@to_project = fork_project(@from_project, @to_user) @to_project = fork_project(@from_project, @to_user)
expect(@existing_project.persisted?).to be_truthy expect(@existing_project.persisted?).to be_truthy
expect(@to_project.errors[:base]).to include("Invalid fork destination") expect(@to_project.errors[:name]).to eq(['has already been taken'])
expect(@to_project.errors[:base]).not_to include("Fork transaction failed.") expect(@to_project.errors[:path]).to eq(['has already been taken'])
end end
end end
...@@ -81,7 +81,7 @@ describe Projects::ForkService do ...@@ -81,7 +81,7 @@ describe Projects::ForkService do
context 'fork project for group when user not owner' do context 'fork project for group when user not owner' do
it 'group developer should fail to fork project into the group' do it 'group developer should fail to fork project into the group' do
to_project = fork_project(@project, @developer, true, @opts) 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
end end
...@@ -91,7 +91,6 @@ describe Projects::ForkService do ...@@ -91,7 +91,6 @@ describe Projects::ForkService do
namespace: @group) namespace: @group)
to_project = fork_project(@project, @group_owner, true, @opts) to_project = fork_project(@project, @group_owner, true, @opts)
expect(existing_project.persisted?).to be_truthy 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[:name]).to eq(['has already been taken'])
expect(to_project.errors[:path]).to eq(['has already been taken']) expect(to_project.errors[:path]).to eq(['has already been taken'])
end end
...@@ -99,10 +98,7 @@ describe Projects::ForkService do ...@@ -99,10 +98,7 @@ describe Projects::ForkService do
end end
def fork_project(from_project, user, fork_success = true, params = {}) def fork_project(from_project, user, fork_success = true, params = {})
context = Projects::ForkService.new(from_project, user, params) allow_any_instance_of(Gitlab::Shell).to receive(:fork_repository).and_return(fork_success)
shell = double('gitlab_shell') Projects::ForkService.new(from_project, user, params).execute
shell.stub(fork_repository: fork_success)
context.stub(gitlab_shell: shell)
context.execute
end end
end end
...@@ -10,19 +10,13 @@ end ...@@ -10,19 +10,13 @@ end
ENV["RAILS_ENV"] ||= 'test' ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__) require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails' require 'rspec/rails'
require 'capybara/rails'
require 'capybara/rspec'
require 'webmock/rspec' require 'webmock/rspec'
require 'email_spec' require 'email_spec'
require 'sidekiq/testing/inline' 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, # Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories. # 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) WebMock.disable_net_connect!(allow_localhost: true)
......
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
...@@ -35,6 +35,20 @@ module ReferenceFilterSpecHelper ...@@ -35,6 +35,20 @@ module ReferenceFilterSpecHelper
described_class.call(html, contexts) described_class.call(html, contexts)
end 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! def allow_cross_reference!
allow_any_instance_of(described_class). allow_any_instance_of(described_class).
to receive(:user_can_reference_project?).and_return(true) to receive(:user_can_reference_project?).and_return(true)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment