Commit b118f648 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'master' of github.com:gitlabhq/gitlabhq

parents d2fffff0 b8433cb8
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.14.0 (unreleased) v 7.14.0 (unreleased)
- Fix multi-line syntax highlighting (Stan Hu)
- Fix network graph when branch name has single quotes (Stan Hu)
- Upgrade gitlab_git to version 7.2.6 to fix Error 500 when creating network graphs (Stan Hu) - Upgrade gitlab_git to version 7.2.6 to fix Error 500 when creating network graphs (Stan Hu)
- Add support for Unicode filenames in relative links (Hiroyuki Sato)
- Fix URL used for refreshing notes if relative_url is present (Bartłomiej Święcki) - Fix URL used for refreshing notes if relative_url is present (Bartłomiej Święcki)
- Fix commit data retrieval when branch name has single quotes (Stan Hu) - Fix commit data retrieval when branch name has single quotes (Stan Hu)
- Fix Error 500 when browsing projects with no HEAD (Stan Hu) - Check that project was actually created rather than just validated in import:repos task (Stan Hu)
- Add rake task 'gitlab:update_commit_count' (Daniel Gerhardt)
- Fix full screen mode for snippet comments (Daniel Gerhardt) - Fix full screen mode for snippet comments (Daniel Gerhardt)
- Fix 404 error in files view after deleting the last file in a repository (Stan Hu) - Fix 404 error in files view after deleting the last file in a repository (Stan Hu)
- Fix the "Reload with full diff" URL button (Stan Hu) - Fix the "Reload with full diff" URL button (Stan Hu)
...@@ -18,6 +20,23 @@ v 7.14.0 (unreleased) ...@@ -18,6 +20,23 @@ v 7.14.0 (unreleased)
- Add support for destroying project milestones (Stan Hu) - Add support for destroying project milestones (Stan Hu)
- Add fetch command to the MR page. - Add fetch command to the MR page.
- Allow custom backup archive permissions - Allow custom backup archive permissions
- Add fetch command to the MR page
- Add project star and fork count, group avatar URL and user/group web URL attributes to API
- Fix bug causing Bitbucket importer to crash when OAuth application had been removed.
- Add fetch command to the MR page.
- Add ability to manage user email addresses via the API.
- Disabled autocapitalize and autocorrect on login field (Daryl Chan)
- Mention group and project name in creation, update and deletion notices (Achilleas Pipinellis)
v 7.13.2
- Fix randomly failed spec
- Create project services on Project creation
- Add admin_merge_request ability to Developer level and up
- Fix Error 500 when browsing projects with no HEAD (Stan Hu)
- Fix labels / assignee / milestone for the merge requests when issues are disabled
- Show the first tab automatically on MergeRequests#new
- Add rake task 'gitlab:update_commit_count' (Daniel Gerhardt)
- Fix Gmail Actions
v 7.13.1 v 7.13.1
- Fix: Label modifications are not reflected in existing notes and in the issue list - Fix: Label modifications are not reflected in existing notes and in the issue list
...@@ -28,6 +47,7 @@ v 7.13.1 ...@@ -28,6 +47,7 @@ v 7.13.1
- Fix: ActionView::Template::Error - Fix: ActionView::Template::Error
- Fix: "Create Merge Request" isn't always shown in event for newly pushed branch - Fix: "Create Merge Request" isn't always shown in event for newly pushed branch
- Fix bug causing "Remove source-branch" option not to work for merge requests from the same project. - Fix bug causing "Remove source-branch" option not to work for merge requests from the same project.
- Render Note field hints consistently for "new" and "edit" forms
v 7.13.0 v 7.13.0
- Remove repository graph log to fix slow cache updates after push event (Stan Hu) - Remove repository graph log to fix slow cache updates after push event (Stan Hu)
......
...@@ -70,7 +70,7 @@ ...@@ -70,7 +70,7 @@
font-family: $monospace_font; font-family: $monospace_font;
white-space: pre; white-space: pre;
word-wrap: normal; word-wrap: normal;
padding: 0; padding: 1px 2px;
} }
kbd { kbd {
......
...@@ -38,6 +38,10 @@ code { ...@@ -38,6 +38,10 @@ code {
} }
} }
a > code {
color: $link-color;
}
/** /**
* Wiki typography * Wiki typography
* *
......
...@@ -18,4 +18,10 @@ class Groups::ApplicationController < ApplicationController ...@@ -18,4 +18,10 @@ class Groups::ApplicationController < ApplicationController
return render_404 return render_404
end end
end end
def authorize_admin_group_member!
unless can?(current_user, :admin_group_member, group)
return render_403
end
end
end end
...@@ -5,6 +5,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -5,6 +5,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
# Authorize # Authorize
before_action :authorize_read_group! before_action :authorize_read_group!
before_action :authorize_admin_group!, except: [:index, :leave] before_action :authorize_admin_group!, except: [:index, :leave]
before_action :authorize_admin_group_member!, only: [:create, :resend_invite]
def index def index
@project = @group.projects.find(params[:project_id]) if params[:project_id] @project = @group.projects.find(params[:project_id]) if params[:project_id]
...@@ -28,6 +29,9 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -28,6 +29,9 @@ class Groups::GroupMembersController < Groups::ApplicationController
def update def update
@member = @group.group_members.find(params[:id]) @member = @group.group_members.find(params[:id])
return render_403 unless can?(current_user, :update_group_member, @member)
@member.update_attributes(member_params) @member.update_attributes(member_params)
end end
......
...@@ -24,7 +24,7 @@ class GroupsController < Groups::ApplicationController ...@@ -24,7 +24,7 @@ class GroupsController < Groups::ApplicationController
if @group.save if @group.save
@group.add_owner(current_user) @group.add_owner(current_user)
redirect_to @group, notice: 'Group was successfully created.' redirect_to @group, notice: "Group '#{@group.name}' was successfully created."
else else
render action: "new" render action: "new"
end end
...@@ -75,7 +75,7 @@ class GroupsController < Groups::ApplicationController ...@@ -75,7 +75,7 @@ class GroupsController < Groups::ApplicationController
def update def update
if @group.update_attributes(group_params) if @group.update_attributes(group_params)
redirect_to edit_group_path(@group), notice: 'Group was successfully updated.' redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated."
else else
render action: "edit" render action: "edit"
end end
...@@ -84,7 +84,7 @@ class GroupsController < Groups::ApplicationController ...@@ -84,7 +84,7 @@ class GroupsController < Groups::ApplicationController
def destroy def destroy
DestroyGroupService.new(@group, current_user).execute DestroyGroupService.new(@group, current_user).execute
redirect_to root_path, notice: 'Group was removed.' redirect_to root_path, alert: "Group '#{@group.name} was deleted."
end end
protected protected
......
...@@ -7,6 +7,10 @@ class Projects::NetworkController < Projects::ApplicationController ...@@ -7,6 +7,10 @@ class Projects::NetworkController < Projects::ApplicationController
before_action :authorize_download_code! before_action :authorize_download_code!
def show def show
@url = namespace_project_network_path(@project.namespace, @project, @ref, @options.merge(format: :json))
@commit_url = namespace_project_commit_path(@project.namespace, @project, 'ae45ca32').gsub("ae45ca32", "%s")
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -24,7 +24,7 @@ class ProjectsController < ApplicationController ...@@ -24,7 +24,7 @@ class ProjectsController < ApplicationController
if @project.saved? if @project.saved?
redirect_to( redirect_to(
project_path(@project), project_path(@project),
notice: 'Project was successfully created.' notice: "Project '#{@project.name}' was successfully created."
) )
else else
render 'new' render 'new'
...@@ -36,11 +36,11 @@ class ProjectsController < ApplicationController ...@@ -36,11 +36,11 @@ class ProjectsController < ApplicationController
respond_to do |format| respond_to do |format|
if status if status
flash[:notice] = 'Project was successfully updated.' flash[:notice] = "Project '#{@project.name}' was successfully updated."
format.html do format.html do
redirect_to( redirect_to(
edit_project_path(@project), edit_project_path(@project),
notice: 'Project was successfully updated.' notice: "Project '#{@project.name}' was successfully updated."
) )
end end
format.js format.js
...@@ -100,7 +100,7 @@ class ProjectsController < ApplicationController ...@@ -100,7 +100,7 @@ class ProjectsController < ApplicationController
return access_denied! unless can?(current_user, :remove_project, @project) return access_denied! unless can?(current_user, :remove_project, @project)
::Projects::DestroyService.new(@project, current_user, {}).execute ::Projects::DestroyService.new(@project, current_user, {}).execute
flash[:alert] = 'Project deleted.' flash[:alert] = "Project '#{@project.name}' was deleted."
if request.referer.include?('/admin') if request.referer.include?('/admin')
redirect_to admin_namespaces_projects_path redirect_to admin_namespaces_projects_path
......
...@@ -233,7 +233,8 @@ class Ability ...@@ -233,7 +233,8 @@ class Ability
if group.has_owner?(user) || user.admin? if group.has_owner?(user) || user.admin?
rules.push(*[ rules.push(*[
:admin_group, :admin_group,
:admin_namespace :admin_namespace,
:admin_group_member
]) ])
end end
...@@ -295,7 +296,7 @@ class Ability ...@@ -295,7 +296,7 @@ class Ability
rules = [] rules = []
target_user = subject.user target_user = subject.user
group = subject.group group = subject.group
can_manage = group_abilities(user, group).include?(:admin_group) can_manage = group_abilities(user, group).include?(:admin_group_member)
if can_manage && (user != target_user) if can_manage && (user != target_user)
rules << :update_group_member rules << :update_group_member
......
...@@ -14,13 +14,14 @@ ...@@ -14,13 +14,14 @@
# default_branch_protection :integer default(2) # default_branch_protection :integer default(2)
# twitter_sharing_enabled :boolean default(TRUE) # twitter_sharing_enabled :boolean default(TRUE)
# restricted_visibility_levels :text # restricted_visibility_levels :text
# version_check_enabled :boolean default(TRUE)
# max_attachment_size :integer default(10), not null # max_attachment_size :integer default(10), not null
# session_expire_delay :integer default(10080), not null
# default_project_visibility :integer # default_project_visibility :integer
# default_snippet_visibility :integer # default_snippet_visibility :integer
# restricted_signup_domains :text # restricted_signup_domains :text
# user_oauth_applications :bool default(TRUE) # user_oauth_applications :boolean default(TRUE)
# after_sign_out_path :string(255) # after_sign_out_path :string(255)
# session_expire_delay :integer default(10080), not null
# #
class ApplicationSetting < ActiveRecord::Base class ApplicationSetting < ActiveRecord::Base
......
# == Schema Information
#
# Table name: audit_events
#
# id :integer not null, primary key
# author_id :integer not null
# type :string(255) not null
# entity_id :integer not null
# entity_type :string(255) not null
# details :text
# created_at :datetime
# updated_at :datetime
#
class AuditEvent < ActiveRecord::Base class AuditEvent < ActiveRecord::Base
serialize :details, Hash serialize :details, Hash
......
...@@ -56,6 +56,12 @@ class Group < Namespace ...@@ -56,6 +56,12 @@ class Group < Namespace
name name
end end
def avatar_url(size = nil)
if avatar.present?
[gitlab_config.url, avatar.url].join
end
end
def owners def owners
@owners ||= group_members.owners.map(&:user) @owners ||= group_members.owners.map(&:user)
end end
......
...@@ -21,12 +21,13 @@ ...@@ -21,12 +21,13 @@
# import_url :string(255) # import_url :string(255)
# visibility_level :integer default(0), not null # visibility_level :integer default(0), not null
# archived :boolean default(FALSE), not null # archived :boolean default(FALSE), not null
# avatar :string(255)
# import_status :string(255) # import_status :string(255)
# repository_size :float default(0.0) # repository_size :float default(0.0)
# star_count :integer default(0), not null # star_count :integer default(0), not null
# import_type :string(255) # import_type :string(255)
# import_source :string(255) # import_source :string(255)
# avatar :string(255) # commit_count :integer default(0)
# #
require 'carrierwave/orm/activerecord' require 'carrierwave/orm/activerecord'
...@@ -36,7 +37,6 @@ class Project < ActiveRecord::Base ...@@ -36,7 +37,6 @@ class Project < ActiveRecord::Base
include Gitlab::ConfigHelper include Gitlab::ConfigHelper
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include Gitlab::VisibilityLevel include Gitlab::VisibilityLevel
include Rails.application.routes.url_helpers
include Referable include Referable
include Sortable include Sortable
...@@ -316,7 +316,7 @@ class Project < ActiveRecord::Base ...@@ -316,7 +316,7 @@ class Project < ActiveRecord::Base
end end
def web_url def web_url
[gitlab_config.url, path_with_namespace].join('/') Rails.application.routes.url_helpers.namespace_project_url(self.namespace, self)
end end
def web_url_without_protocol def web_url_without_protocol
...@@ -433,7 +433,7 @@ class Project < ActiveRecord::Base ...@@ -433,7 +433,7 @@ class Project < ActiveRecord::Base
if avatar.present? if avatar.present?
[gitlab_config.url, avatar.url].join [gitlab_config.url, avatar.url].join
elsif avatar_in_git elsif avatar_in_git
[gitlab_config.url, namespace_project_avatar_path(namespace, self)].join Rails.application.routes.url_helpers.namespace_project_avatar_url(namespace, self)
end end
end end
...@@ -571,7 +571,7 @@ class Project < ActiveRecord::Base ...@@ -571,7 +571,7 @@ class Project < ActiveRecord::Base
end end
def http_url_to_repo def http_url_to_repo
[gitlab_config.url, '/', path_with_namespace, '.git'].join('') "#{web_url}.git"
end end
# Check if current branch name is marked as protected in the system # Check if current branch name is marked as protected in the system
...@@ -705,14 +705,14 @@ class Project < ActiveRecord::Base ...@@ -705,14 +705,14 @@ class Project < ActiveRecord::Base
ensure_satellite_exists ensure_satellite_exists
true true
else else
errors.add(:base, 'Failed to fork repository') errors.add(:base, 'Failed to fork repository via gitlab-shell')
false false
end end
else else
if gitlab_shell.add_repository(path_with_namespace) if gitlab_shell.add_repository(path_with_namespace)
true true
else else
errors.add(:base, 'Failed to create repository') errors.add(:base, 'Failed to create repository via gitlab-shell')
false false
end end
end end
......
# == Schema Information
#
# Table name: audit_events
#
# id :integer not null, primary key
# author_id :integer not null
# type :string(255) not null
# entity_id :integer not null
# entity_type :string(255) not null
# details :text
# created_at :datetime
# updated_at :datetime
#
class SecurityEvent < AuditEvent class SecurityEvent < AuditEvent
end end
...@@ -57,6 +57,7 @@ ...@@ -57,6 +57,7 @@
# otp_backup_codes :text # otp_backup_codes :text
# public_email :string(255) default(""), not null # public_email :string(255) default(""), not null
# dashboard :integer default(0) # dashboard :integer default(0)
# project_view :integer default(0)
# #
require 'carrierwave/orm/activerecord' require 'carrierwave/orm/activerecord'
......
...@@ -51,6 +51,7 @@ ...@@ -51,6 +51,7 @@
= paginate @projects, param_name: 'projects_page', theme: 'gitlab' = paginate @projects, param_name: 'projects_page', theme: 'gitlab'
.col-md-6 .col-md-6
- if can?(current_user, :admin_group_member, @group)
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
Add user(s) to the group: Add user(s) to the group:
...@@ -86,6 +87,7 @@ ...@@ -86,6 +87,7 @@
(invited) (invited)
%span.pull-right.light %span.pull-right.light
= member.human_access = member.human_access
- if can?(current_user, :destroy_group_member, member)
= link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-minus.fa-inverse %i.fa.fa-minus.fa-inverse
.panel-footer .panel-footer
......
= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| = form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
= f.text_field :login, class: "form-control top", placeholder: "Username or Email", autofocus: "autofocus" = f.text_field :login, class: "form-control top", placeholder: "Username or Email", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off"
= f.password_field :password, class: "form-control bottom", placeholder: "Password" = f.password_field :password, class: "form-control bottom", placeholder: "Password"
- if devise_mapping.rememberable? - if devise_mapping.rememberable?
.remember-me.checkbox .remember-me.checkbox
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
= link_to member.created_by.name, user_path(member.created_by) = link_to member.created_by.name, user_path(member.created_by)
= time_ago_with_tooltip(member.created_at) = time_ago_with_tooltip(member.created_at)
- if show_controls && can?(current_user, :admin_group, @group) - if show_controls && can?(current_user, :admin_group_member, member)
= link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do = link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
Resend invite Resend invite
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
= search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input' } = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input' }
= button_tag 'Search', class: 'btn' = button_tag 'Search', class: 'btn'
- if current_user && current_user.can?(:admin_group, @group) - if current_user && current_user.can?(:admin_group_member, @group)
.pull-right .pull-right
= button_tag class: 'btn btn-new js-toggle-button', type: 'button' do = button_tag class: 'btn btn-new js-toggle-button', type: 'button' do
Add members Add members
......
...@@ -6,14 +6,13 @@ ...@@ -6,14 +6,13 @@
- @key.errors.full_messages.each do |msg| - @key.errors.full_messages.each do |msg|
%li= msg %li= msg
.form-group
= f.label :title, class: 'control-label'
.col-sm-10= f.text_field :title, class: "form-control"
.form-group .form-group
= f.label :key, class: 'control-label' = f.label :key, class: 'control-label'
.col-sm-10 .col-sm-10
= f.text_area :key, class: "form-control", rows: 8 = f.text_area :key, class: "form-control", rows: 8
.form-group
= f.label :title, class: 'control-label'
.col-sm-10= f.text_field :title, class: "form-control"
.form-actions .form-actions
= f.submit 'Add key', class: "btn btn-create" = f.submit 'Add key', class: "btn btn-create"
......
...@@ -17,9 +17,9 @@ ...@@ -17,9 +17,9 @@
:javascript :javascript
network_graph = new Network({ network_graph = new Network({
url: '#{namespace_project_network_path(@project.namespace, @project, @ref, @options.merge(format: :json))}', url: "#{escape_javascript(@url)}",
commit_url: '#{namespace_project_commit_path(@project.namespace, @project, 'ae45ca32').gsub("ae45ca32", "%s")}', commit_url: "#{escape_javascript(@commit_url)}",
ref: '#{@ref}', ref: "#{escape_javascript(@ref)}",
commit_id: '#{@commit.id}' commit_id: '#{@commit.id}'
}) })
new ShortcutsNetwork(network_graph.branch_graph) new ShortcutsNetwork(network_graph.branch_graph)
...@@ -397,6 +397,138 @@ Parameters: ...@@ -397,6 +397,138 @@ Parameters:
Will return `200 OK` on success, or `404 Not found` if either user or key cannot be found. Will return `200 OK` on success, or `404 Not found` if either user or key cannot be found.
## List emails
Get a list of currently authenticated user's emails.
```
GET /user/emails
```
```json
[
{
"id": 1,
"email": "email@example.com"
},
{
"id": 3,
"email": "email2@example.com"
}
]
```
Parameters:
- **none**
## List emails for user
Get a list of a specified user's emails. Available only for admin
```
GET /users/:uid/emails
```
Parameters:
- `uid` (required) - id of specified user
## Single email
Get a single email.
```
GET /user/emails/:id
```
Parameters:
- `id` (required) - email ID
```json
{
"id": 1,
"email": "email@example.com"
}
```
## Add email
Creates a new email owned by the currently authenticated user.
```
POST /user/emails
```
Parameters:
- `email` (required) - email address
```json
{
"id": 4,
"email": "email@example.com"
}
```
Will return created email with status `201 Created` on success. If an
error occurs a `400 Bad Request` is returned with a message explaining the error:
```json
{
"message": {
"email": [
"has already been taken"
]
}
}
```
## Add email for user
Create new email owned by specified user. Available only for admin
```
POST /users/:id/emails
```
Parameters:
- `id` (required) - id of specified user
- `email` (required) - email address
Will return created email with status `201 Created` on success, or `404 Not found` on fail.
## Delete email for current user
Deletes email owned by currently authenticated user.
This is an idempotent function and calling it on a email that is already deleted
or not available results in `200 OK`.
```
DELETE /user/emails/:id
```
Parameters:
- `id` (required) - email ID
## Delete email for given user
Deletes email owned by a specified user. Available only for admin.
```
DELETE /users/:uid/emails/:id
```
Parameters:
- `uid` (required) - id of specified user
- `id` (required) - email ID
Will return `200 OK` on success, or `404 Not found` if either user or email cannot be found.
## Block user ## Block user
Blocks the specified user. Available only for admin. Blocks the specified user. Available only for admin.
......
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
To enable the Twitter OmniAuth provider you must register your application with Twitter. Twitter will generate a client ID and secret key for you to use. To enable the Twitter OmniAuth provider you must register your application with Twitter. Twitter will generate a client ID and secret key for you to use.
1. Sign in to [Twitter Developers](https://dev.twitter.com/) area. 1. Sign in to [Twitter Application Management](https://apps.twitter.com/).
1. Hover over the avatar in the top right corner and select "My applications."
1. Select "Create new app" 1. Select "Create new app"
...@@ -14,18 +12,18 @@ To enable the Twitter OmniAuth provider you must register your application with ...@@ -14,18 +12,18 @@ To enable the Twitter OmniAuth provider you must register your application with
- Description: Create a description. - Description: Create a description.
- Website: The URL to your GitLab installation. 'https://gitlab.example.com' - Website: The URL to your GitLab installation. 'https://gitlab.example.com'
- Callback URL: 'https://gitlab.example.com/users/auth/twitter/callback' - Callback URL: 'https://gitlab.example.com/users/auth/twitter/callback'
- Agree to the "Rules of the Road." - Agree to the "Developer Agreement".
![Twitter App Details](twitter_app_details.png) ![Twitter App Details](twitter_app_details.png)
1. Select "Create your Twitter application." 1. Select "Create your Twitter application."
1. Select the "Settings" tab. 1. Select the "Settings" tab.
1. Underneath the Callback URL check the box next to "Allow this application to be used to Sign in the Twitter." 1. Underneath the Callback URL check the box next to "Allow this application to be used to Sign in with Twitter."
1. Select "Update settings" at the bottom to save changes. 1. Select "Update settings" at the bottom to save changes.
1. Select the "API Keys" tab. 1. Select the "Keys and Access Tokens" tab.
1. You should now see an API key and API secret (see screenshot). Keep this page open as you continue configuration. 1. You should now see an API key and API secret (see screenshot). Keep this page open as you continue configuration.
......
...@@ -31,7 +31,7 @@ We think there is still room for improvement and will detail a set of practices ...@@ -31,7 +31,7 @@ We think there is still room for improvement and will detail a set of practices
## Git flow and its problems ## Git flow and its problems
[![Git Flow timeline by Vincent Driessen, used with permission](gitdashflow.png) ![Git Flow timeline by Vincent Driessen, used with permission](gitdashflow.png)
Git flow was one of the first proposals to use git branches and it has gotten a lot of attention. Git flow was one of the first proposals to use git branches and it has gotten a lot of attention.
It advocates a master branch and a separate develop branch as well as supporting branches for features, releases and hotfixes. It advocates a master branch and a separate develop branch as well as supporting branches for features, releases and hotfixes.
...@@ -54,7 +54,7 @@ And doing releases doesn't automatically mean also doing hotfixes. ...@@ -54,7 +54,7 @@ And doing releases doesn't automatically mean also doing hotfixes.
![Master branch with feature branches merged in](github_flow.png) ![Master branch with feature branches merged in](github_flow.png)
In reaction to git flow a simpler alternative was detailed, [GitHub flow](https://guides.github.com/introduction/flow/index.html). In reaction to git flow a simpler alternative was detailed, [GitHub flow](https://guides.github.com/introduction/flow/index.html).
This flow has only feature branches and a master branch. This flow has only feature branches and a master branch.
This is very simple and clean, many organizations have adopted it with great success. This is very simple and clean, many organizations have adopted it with great success.
Atlassian recommends [a similar strategy](http://blogs.atlassian.com/2014/01/simple-git-workflow-simple/) although they rebase feature branches. Atlassian recommends [a similar strategy](http://blogs.atlassian.com/2014/01/simple-git-workflow-simple/) although they rebase feature branches.
...@@ -131,7 +131,7 @@ When you feel comfortable with it to be merged you assign it to the person that ...@@ -131,7 +131,7 @@ When you feel comfortable with it to be merged you assign it to the person that
There is room for more feedback and after the assigned person feels comfortable with the result the branch is merged. There is room for more feedback and after the assigned person feels comfortable with the result the branch is merged.
If the assigned person does not feel comfortable they can close the merge request without merging. If the assigned person does not feel comfortable they can close the merge request without merging.
In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/permissions/permissions.md). In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://doc.gitlab.com/ce/permissions/permissions.html).
So if you want to merge it into a protected branch you assign it to someone with master authorizations. So if you want to merge it into a protected branch you assign it to someone with master authorizations.
## Issues with GitLab flow ## Issues with GitLab flow
...@@ -216,7 +216,7 @@ This prevents creating a merge commit when merging master into your feature bran ...@@ -216,7 +216,7 @@ This prevents creating a merge commit when merging master into your feature bran
However, just like with squashing you should never rebase commits you have pushed to a remote server. However, just like with squashing you should never rebase commits you have pushed to a remote server.
This makes it impossible to rebase work in progress that you already shared with your team which is something we recommend. This makes it impossible to rebase work in progress that you already shared with your team which is something we recommend.
When using rebase to keep your feature branch updated you [need to resolve similar conflicts again and again](http://blogs.atlassian.com/2013/10/git-team-workflows-merge-or-rebase/). When using rebase to keep your feature branch updated you [need to resolve similar conflicts again and again](http://blogs.atlassian.com/2013/10/git-team-workflows-merge-or-rebase/).
You can reuse recorded resolutions (rerere) sometimes, but with without rebasing you only have to solve the conflicts one time and you’re set. You can reuse recorded resolutions (rerere) sometimes, but without rebasing you only have to solve the conflicts one time and you’re set.
There has to be a better way to avoid many merge commits. There has to be a better way to avoid many merge commits.
The way to prevent creating many merge commits is to not frequently merge master into the feature branch. The way to prevent creating many merge commits is to not frequently merge master into the feature branch.
......
...@@ -10,6 +10,11 @@ Feature: Project Network Graph ...@@ -10,6 +10,11 @@ Feature: Project Network Graph
And page should select "master" in select box And page should select "master" in select box
And page should have "master" on graph And page should have "master" on graph
@javascript
Scenario: I should see project network with 'test' branch
When I visit project network page on branch 'test'
Then page should have 'test' on graph
@javascript @javascript
Scenario: I should switch "branch" and "tag" Scenario: I should switch "branch" and "tag"
When I switch ref to "feature" When I switch ref to "feature"
......
...@@ -11,8 +11,12 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps ...@@ -11,8 +11,12 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
# Stub Graph max_size to speed up test (10 commits vs. 650) # Stub Graph max_size to speed up test (10 commits vs. 650)
Network::Graph.stub(max_count: 10) Network::Graph.stub(max_count: 10)
project = Project.find_by(name: "Shop") @project = Project.find_by(name: "Shop")
visit namespace_project_network_path(project.namespace, project, "master") visit namespace_project_network_path(@project.namespace, @project, "master")
end
step "I visit project network page on branch 'test'" do
visit namespace_project_network_path(@project.namespace, @project, "'test'")
end end
step 'page should select "master" in select box' do step 'page should select "master" in select box' do
...@@ -29,6 +33,12 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps ...@@ -29,6 +33,12 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
end end
end end
step "page should have 'test' on graph" do
page.within '.network-graph' do
expect(page).to have_content "'test'"
end
end
When 'I switch ref to "feature"' do When 'I switch ref to "feature"' do
select 'feature', from: 'ref' select 'feature', from: 'ref'
sleep 2 sleep 2
......
...@@ -6,6 +6,10 @@ module API ...@@ -6,6 +6,10 @@ module API
class UserBasic < UserSafe class UserBasic < UserSafe
expose :id, :state, :avatar_url expose :id, :state, :avatar_url
expose :web_url do |user, options|
Rails.application.routes.url_helpers.user_url(user)
end
end end
class User < UserBasic class User < UserBasic
...@@ -31,6 +35,10 @@ module API ...@@ -31,6 +35,10 @@ module API
expose :private_token expose :private_token
end end
class Email < Grape::Entity
expose :id, :email
end
class Hook < Grape::Entity class Hook < Grape::Entity
expose :id, :url, :created_at expose :id, :url, :created_at
end end
...@@ -59,6 +67,7 @@ module API ...@@ -59,6 +67,7 @@ module API
expose :namespace expose :namespace
expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? } expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? }
expose :avatar_url expose :avatar_url
expose :star_count, :forks_count
end end
class ProjectMember < UserBasic class ProjectMember < UserBasic
...@@ -69,6 +78,11 @@ module API ...@@ -69,6 +78,11 @@ module API
class Group < Grape::Entity class Group < Grape::Entity
expose :id, :name, :path, :description expose :id, :name, :path, :description
expose :avatar_url
expose :web_url do |group, options|
Rails.application.routes.url_helpers.group_url(group)
end
end end
class GroupDetail < Group class GroupDetail < Group
......
...@@ -185,6 +185,65 @@ module API ...@@ -185,6 +185,65 @@ module API
end end
end end
# Add email to a specified user. Only available to admin users.
#
# Parameters:
# id (required) - The ID of a user
# email (required) - Email address
# Example Request:
# POST /users/:id/emails
post ":id/emails" do
authenticated_as_admin!
required_attributes! [:email]
user = User.find(params[:id])
attrs = attributes_for_keys [:email]
email = user.emails.new attrs
if email.save
NotificationService.new.new_email(email)
present email, with: Entities::Email
else
render_validation_error!(email)
end
end
# Get emails of a specified user. Only available to admin users.
#
# Parameters:
# uid (required) - The ID of a user
# Example Request:
# GET /users/:uid/emails
get ':uid/emails' do
authenticated_as_admin!
user = User.find_by(id: params[:uid])
not_found!('User') unless user
present user.emails, with: Entities::Email
end
# Delete existing email of a specified user. Only available to admin
# users.
#
# Parameters:
# uid (required) - The ID of a user
# id (required) - Email ID
# Example Request:
# DELETE /users/:uid/emails/:id
delete ':uid/emails/:id' do
authenticated_as_admin!
user = User.find_by(id: params[:uid])
not_found!('User') unless user
begin
email = user.emails.find params[:id]
email.destroy
user.update_secondary_emails!
rescue ActiveRecord::RecordNotFound
not_found!('Email')
end
end
# Delete user. Available only for admin # Delete user. Available only for admin
# #
# Example Request: # Example Request:
...@@ -289,6 +348,58 @@ module API ...@@ -289,6 +348,58 @@ module API
rescue rescue
end end
end end
# Get currently authenticated user's emails
#
# Example Request:
# GET /user/emails
get "emails" do
present current_user.emails, with: Entities::Email
end
# Get single email owned by currently authenticated user
#
# Example Request:
# GET /user/emails/:id
get "emails/:id" do
email = current_user.emails.find params[:id]
present email, with: Entities::Email
end
# Add new email to currently authenticated user
#
# Parameters:
# email (required) - Email address
# Example Request:
# POST /user/emails
post "emails" do
required_attributes! [:email]
attrs = attributes_for_keys [:email]
email = current_user.emails.new attrs
if email.save
NotificationService.new.new_email(email)
present email, with: Entities::Email
else
render_validation_error!(email)
end
end
# Delete existing email of currently authenticated user
#
# Parameters:
# id (required) - EMail ID
# Example Request:
# DELETE /user/emails/:id
delete "emails/:id" do
begin
email = current_user.emails.find params[:id]
email.destroy
current_user.update_secondary_emails!
rescue
end
end
end end
end end
end end
...@@ -7,7 +7,11 @@ module Backup ...@@ -7,7 +7,11 @@ module Backup
def initialize def initialize
@config = YAML.load_file(File.join(Rails.root,'config','database.yml'))[Rails.env] @config = YAML.load_file(File.join(Rails.root,'config','database.yml'))[Rails.env]
@db_dir = File.join(Gitlab.config.backup.path, 'db') @db_dir = File.join(Gitlab.config.backup.path, 'db')
FileUtils.mkdir_p(@db_dir) unless Dir.exists?(@db_dir) FileUtils.rm_rf(@db_dir)
# Ensure the parent dir of @db_dir exists
FileUtils.mkdir_p(Gitlab.config.backup.path)
# Fail if somebody raced to create @db_dir before us
FileUtils.mkdir(@db_dir, mode: 0700)
end end
def dump def dump
...@@ -25,7 +29,6 @@ module Backup ...@@ -25,7 +29,6 @@ module Backup
abort 'Backup failed' unless success abort 'Backup failed' unless success
$progress.print 'Compressing database ... ' $progress.print 'Compressing database ... '
FileUtils.rm_f db_file_name_gz
success = system('gzip', db_file_name) success = system('gzip', db_file_name)
report_success(success) report_success(success)
abort 'Backup failed: compress error' unless success abort 'Backup failed: compress error' unless success
......
...@@ -16,8 +16,6 @@ module Backup ...@@ -16,8 +16,6 @@ module Backup
file << s.to_yaml.gsub(/^---\n/,'') file << s.to_yaml.gsub(/^---\n/,'')
end end
FileUtils.chmod(0700, folders_to_backup)
# create archive # create archive
$progress.print "Creating backup archive: #{tar_file} ... " $progress.print "Creating backup archive: #{tar_file} ... "
# Set file permissions on open to prevent chmod races. # Set file permissions on open to prevent chmod races.
......
...@@ -130,7 +130,10 @@ module Backup ...@@ -130,7 +130,10 @@ module Backup
def prepare def prepare
FileUtils.rm_rf(backup_repos_path) FileUtils.rm_rf(backup_repos_path)
FileUtils.mkdir_p(backup_repos_path) # Ensure the parent dir of backup_repos_path exists
FileUtils.mkdir_p(Gitlab.config.backup.path)
# Fail if somebody raced to create backup_repos_path before us
FileUtils.mkdir(backup_repos_path, mode: 0700)
end end
def silent def silent
......
...@@ -10,7 +10,11 @@ module Backup ...@@ -10,7 +10,11 @@ module Backup
# Copy uploads from public/uploads to backup/uploads # Copy uploads from public/uploads to backup/uploads
def dump def dump
FileUtils.mkdir_p(backup_uploads_dir) FileUtils.rm_rf(backup_uploads_dir)
# Ensure the parent dir of backup_uploads_dir exists
FileUtils.mkdir_p(Gitlab.config.backup.path)
# Fail if somebody raced to create backup_uploads_dir before us
FileUtils.mkdir(backup_uploads_dir, mode: 0700)
FileUtils.cp_r(app_uploads_dir, backup_dir) FileUtils.cp_r(app_uploads_dir, backup_dir)
end end
......
...@@ -98,15 +98,25 @@ module Gitlab ...@@ -98,15 +98,25 @@ module Gitlab
# #
# Returns a String # Returns a String
def path_type(path) def path_type(path)
if repository.tree(current_sha, path).entries.any? unescaped_path = Addressable::URI.unescape(path)
if tree?(unescaped_path)
'tree' 'tree'
elsif repository.blob_at(current_sha, path).try(:image?) elsif image?(unescaped_path)
'raw' 'raw'
else else
'blob' 'blob'
end end
end end
def tree?(path)
repository.tree(current_sha, path).entries.any?
end
def image?(path)
repository.blob_at(current_sha, path).try(:image?)
end
def current_sha def current_sha
context[:commit].try(:id) || context[:commit].try(:id) ||
ref ? repository.commit(ref).try(:sha) : repository.head_commit.sha ref ? repository.commit(ref).try(:sha) : repository.head_commit.sha
......
...@@ -148,6 +148,12 @@ module Rouge ...@@ -148,6 +148,12 @@ module Rouge
end end
end end
def wrap_values(val, element)
lines = val.split("\n")
lines = lines.map{ |x| "<span #{element}>#{x}</span>" }
lines.join("\n")
end
def span(tok, val) def span(tok, val)
# http://stackoverflow.com/a/1600584/2587286 # http://stackoverflow.com/a/1600584/2587286
val = CGI.escapeHTML(val) val = CGI.escapeHTML(val)
...@@ -155,11 +161,13 @@ module Rouge ...@@ -155,11 +161,13 @@ module Rouge
if tok.shortname.empty? if tok.shortname.empty?
val val
else else
# In the case of multi-line values (e.g. comments), we need to apply
# styling to each line since span elements are inline.
if @inline_theme if @inline_theme
rules = @inline_theme.style_for(tok).rendered_rules rules = @inline_theme.style_for(tok).rendered_rules
"<span style=\"#{rules.to_a.join(';')}\">#{val}</span>" wrap_values(val, "style=\"#{rules.to_a.join(';')}\"")
else else
"<span class=\"#{tok.shortname}\">#{val}</span>" wrap_values(val, "class=\"#{tok.shortname}\"")
end end
end end
end end
......
...@@ -485,7 +485,8 @@ namespace :gitlab do ...@@ -485,7 +485,8 @@ namespace :gitlab do
if project.empty_repo? if project.empty_repo?
puts "repository is empty".magenta puts "repository is empty".magenta
elsif File.realpath(project_hook_directory) == File.realpath(gitlab_shell_hooks_path) elsif File.directory?(project_hook_directory) && File.directory?(gitlab_shell_hooks_path) &&
(File.realpath(project_hook_directory) == File.realpath(gitlab_shell_hooks_path))
puts 'ok'.green puts 'ok'.green
else else
puts "wrong or missing hooks".red puts "wrong or missing hooks".red
...@@ -806,4 +807,3 @@ namespace :gitlab do ...@@ -806,4 +807,3 @@ namespace :gitlab do
end end
end end
end end
...@@ -62,11 +62,11 @@ namespace :gitlab do ...@@ -62,11 +62,11 @@ namespace :gitlab do
project = Projects::CreateService.new(user, project_params).execute project = Projects::CreateService.new(user, project_params).execute
if project.valid? if project.persisted?
puts " * Created #{project.name} (#{repo_path})".green puts " * Created #{project.name} (#{repo_path})".green
else else
puts " * Failed trying to create #{project.name} (#{repo_path})".red puts " * Failed trying to create #{project.name} (#{repo_path})".red
puts " Validation Errors: #{project.errors.messages}".red puts " Errors: #{project.errors.messages}".red
end end
end end
end end
......
...@@ -21,12 +21,13 @@ ...@@ -21,12 +21,13 @@
# import_url :string(255) # import_url :string(255)
# visibility_level :integer default(0), not null # visibility_level :integer default(0), not null
# archived :boolean default(FALSE), not null # archived :boolean default(FALSE), not null
# avatar :string(255)
# import_status :string(255) # import_status :string(255)
# repository_size :float default(0.0) # repository_size :float default(0.0)
# star_count :integer default(0), not null # star_count :integer default(0), not null
# import_type :string(255) # import_type :string(255)
# import_source :string(255) # import_source :string(255)
# avatar :string(255) # commit_count :integer default(0)
# #
FactoryGirl.define do FactoryGirl.define do
......
...@@ -17,410 +17,215 @@ require 'erb' ...@@ -17,410 +17,215 @@ require 'erb'
# -> Post-process HTML # -> Post-process HTML
# -> `gfm_with_options` helper # -> `gfm_with_options` helper
# -> HTML::Pipeline # -> HTML::Pipeline
# -> Sanitize # -> SanitizationFilter
# -> RelativeLink # -> Other filters, depending on pipeline
# -> Emoji
# -> Table of Contents
# -> Autolinks
# -> Rinku (http, https, ftp)
# -> Other schemes
# -> ExternalLink
# -> References
# -> TaskList
# -> `html_safe` # -> `html_safe`
# -> Template # -> Template
# #
# See the MarkdownFeature class for setup details. # See the MarkdownFeature class for setup details.
describe 'GitLab Markdown', feature: true do describe 'GitLab Markdown', feature: true do
include ActionView::Helpers::TagHelper
include ActionView::Helpers::UrlHelper
include Capybara::Node::Matchers include Capybara::Node::Matchers
include GitlabMarkdownHelper include GitlabMarkdownHelper
include MarkdownMatchers
# `markdown` calls these two methods # Sometimes it can be useful to see the parsed output of the Markdown document
def current_user # for debugging. Call this method to write the output to
@feat.user # `tmp/capybara/<filename>.html`.
end def write_markdown(filename = 'markdown_spec')
File.open(Rails.root.join("tmp/capybara/#{filename}.html"), 'w') do |file|
def user_color_scheme_class file.puts @html
:white
end
# Let's only parse this thing once
before(:all) do
@feat = MarkdownFeature.new
# `markdown` expects a `@project` variable
@project = @feat.project
@md = markdown(@feat.raw_markdown)
@doc = Nokogiri::HTML::DocumentFragment.parse(@md)
end end
after(:all) do
@feat.teardown
end end
# Given a header ID, goes to that element's parent (the header itself), then def doc(html = @html)
# its next sibling element (the body). Nokogiri::HTML::DocumentFragment.parse(html)
def get_section(id)
@doc.at_css("##{id}").parent.next_element
end end
# Sometimes it can be useful to see the parsed output of the Markdown document # Shared behavior that all pipelines should exhibit
# for debugging. Uncomment this block to write the output to shared_examples 'all pipelines' do
# tmp/capybara/markdown_spec.html. describe 'Redcarpet extensions' do
#
# it 'writes to a file' do
# File.open(Rails.root.join('tmp/capybara/markdown_spec.html'), 'w') do |file|
# file.puts @md
# end
# end
describe 'Markdown' do
describe 'No Intra Emphasis' do
it 'does not parse emphasis inside of words' do it 'does not parse emphasis inside of words' do
body = get_section('no-intra-emphasis') expect(doc.to_html).not_to match('foo<em>bar</em>baz')
expect(body.to_html).not_to match('foo<em>bar</em>baz')
end
end end
describe 'Tables' do
it 'parses table Markdown' do it 'parses table Markdown' do
body = get_section('tables') aggregate_failures do
expect(body).to have_selector('th:contains("Header")') expect(doc).to have_selector('th:contains("Header")')
expect(body).to have_selector('th:contains("Row")') expect(doc).to have_selector('th:contains("Row")')
expect(body).to have_selector('th:contains("Example")') expect(doc).to have_selector('th:contains("Example")')
end
end end
it 'allows Markdown in tables' do it 'allows Markdown in tables' do
expect(@doc.at_css('td:contains("Baz")').children.to_html). expect(doc.at_css('td:contains("Baz")').children.to_html).
to eq '<strong>Baz</strong>' to eq '<strong>Baz</strong>'
end end
end
describe 'Fenced Code Blocks' do
it 'parses fenced code blocks' do it 'parses fenced code blocks' do
expect(@doc).to have_selector('pre.code.highlight.white.c') aggregate_failures do
expect(@doc).to have_selector('pre.code.highlight.white.python') expect(doc).to have_selector('pre.code.highlight.white.c')
expect(doc).to have_selector('pre.code.highlight.white.python')
end end
end end
describe 'Strikethrough' do
it 'parses strikethroughs' do it 'parses strikethroughs' do
expect(@doc).to have_selector(%{del:contains("and this text doesn't")}) expect(doc).to have_selector(%{del:contains("and this text doesn't")})
end
end end
describe 'Superscript' do
it 'parses superscript' do it 'parses superscript' do
body = get_section('superscript') expect(doc).to have_selector('sup', count: 2)
expect(body.to_html).to match('1<sup>st</sup>')
expect(body.to_html).to match('2<sup>nd</sup>')
end
end end
end end
describe 'HTML::Pipeline' do
describe 'SanitizationFilter' do describe 'SanitizationFilter' do
it 'uses a permissive whitelist' do it 'permits b elements' do
expect(@doc).to have_selector('b:contains("b tag")') expect(doc).to have_selector('b:contains("b tag")')
expect(@doc).to have_selector('em:contains("em tag")')
expect(@doc).to have_selector('code:contains("code tag")')
expect(@doc).to have_selector('kbd:contains("s")')
expect(@doc).to have_selector('strike:contains(Emoji)')
expect(@doc).to have_selector('img[src*="smile.png"]')
expect(@doc).to have_selector('br')
expect(@doc).to have_selector('hr')
end end
it 'permits span elements' do it 'permits em elements' do
expect(@doc).to have_selector('span:contains("span tag")') expect(doc).to have_selector('em:contains("em tag")')
end end
it 'permits table alignment' do it 'permits code elements' do
expect(@doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center' expect(doc).to have_selector('code:contains("code tag")')
expect(@doc.at_css('th:contains("Row")')['style']).to eq 'text-align: right'
expect(@doc.at_css('th:contains("Example")')['style']).to eq 'text-align: left'
expect(@doc.at_css('td:contains("Foo")')['style']).to eq 'text-align: center'
expect(@doc.at_css('td:contains("Bar")')['style']).to eq 'text-align: right'
expect(@doc.at_css('td:contains("Baz")')['style']).to eq 'text-align: left'
end end
it 'removes `rel` attribute from links' do it 'permits kbd elements' do
body = get_section('sanitizationfilter') expect(doc).to have_selector('kbd:contains("s")')
expect(body).not_to have_selector('a[rel="bookmark"]')
end end
it "removes `href` from `a` elements if it's fishy" do it 'permits strike elements' do
expect(@doc).not_to have_selector('a[href*="javascript"]') expect(doc).to have_selector('strike:contains(Emoji)')
end
end end
describe 'Escaping' do it 'permits img elements' do
let(:table) { @doc.css('table').last.at_css('tbody') } expect(doc).to have_selector('img[src*="smile.png"]')
it 'escapes non-tag angle brackets' do
expect(table.at_xpath('.//tr[1]/td[3]').inner_html).to eq '1 &lt; 3 &amp; 5'
end
end end
describe 'Edge Cases' do it 'permits br elements' do
it 'allows markup inside link elements' do expect(doc).to have_selector('br')
expect(@doc.at_css('a[href="#link-emphasis"]').to_html). end
to eq %{<a href="#link-emphasis"><em>text</em></a>}
expect(@doc.at_css('a[href="#link-strong"]').to_html).
to eq %{<a href="#link-strong"><strong>text</strong></a>}
expect(@doc.at_css('a[href="#link-code"]').to_html). it 'permits hr elements' do
to eq %{<a href="#link-code"><code>text</code></a>} expect(doc).to have_selector('hr')
end end
it 'permits span elements' do
expect(doc).to have_selector('span:contains("span tag")')
end end
describe 'EmojiFilter' do it 'permits style attribute in th elements' do
it 'parses Emoji' do aggregate_failures do
expect(@doc).to have_selector('img.emoji', count: 10) expect(doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center'
expect(doc.at_css('th:contains("Row")')['style']).to eq 'text-align: right'
expect(doc.at_css('th:contains("Example")')['style']).to eq 'text-align: left'
end end
end end
describe 'TableOfContentsFilter' do it 'permits style attribute in td elements' do
it 'creates anchors inside header elements' do aggregate_failures do
expect(@doc).to have_selector('h1 a#gitlab-markdown') expect(doc.at_css('td:contains("Foo")')['style']).to eq 'text-align: center'
expect(@doc).to have_selector('h2 a#markdown') expect(doc.at_css('td:contains("Bar")')['style']).to eq 'text-align: right'
expect(@doc).to have_selector('h3 a#autolinkfilter') expect(doc.at_css('td:contains("Baz")')['style']).to eq 'text-align: left'
end end
end end
describe 'AutolinkFilter' do it 'removes `rel` attribute from links' do
let(:list) { get_section('autolinkfilter').next_element } expect(doc).not_to have_selector('a[rel="bookmark"]')
def item(index)
list.at_css("li:nth-child(#{index})")
end end
it 'autolinks http://' do it "removes `href` from `a` elements if it's fishy" do
expect(item(1).children.first.name).to eq 'a' expect(doc).not_to have_selector('a[href*="javascript"]')
expect(item(1).children.first['href']).to eq 'http://about.gitlab.com/'
end end
it 'autolinks https://' do
expect(item(2).children.first.name).to eq 'a'
expect(item(2).children.first['href']).to eq 'https://google.com/'
end end
it 'autolinks ftp://' do describe 'Escaping' do
expect(item(3).children.first.name).to eq 'a' it 'escapes non-tag angle brackets' do
expect(item(3).children.first['href']).to eq 'ftp://ftp.us.debian.org/debian/' table = doc.css('table').last.at_css('tbody')
expect(table.at_xpath('.//tr[1]/td[3]').inner_html).to eq '1 &lt; 3 &amp; 5'
end end
it 'autolinks smb://' do
expect(item(4).children.first.name).to eq 'a'
expect(item(4).children.first['href']).to eq 'smb://foo/bar/baz'
end end
it 'autolinks irc://' do describe 'Edge Cases' do
expect(item(5).children.first.name).to eq 'a' it 'allows markup inside link elements' do
expect(item(5).children.first['href']).to eq 'irc://irc.freenode.net/git' aggregate_failures do
end expect(doc.at_css('a[href="#link-emphasis"]').to_html).
to eq %{<a href="#link-emphasis"><em>text</em></a>}
it 'autolinks short, invalid URLs' do expect(doc.at_css('a[href="#link-strong"]').to_html).
expect(item(6).children.first.name).to eq 'a' to eq %{<a href="#link-strong"><strong>text</strong></a>}
expect(item(6).children.first['href']).to eq 'http://localhost:3000'
end
%w(code a kbd).each do |elem| expect(doc.at_css('a[href="#link-code"]').to_html).
it "ignores links inside '#{elem}' element" do to eq %{<a href="#link-code"><code>text</code></a>}
body = get_section('autolinkfilter')
expect(body).not_to have_selector("#{elem} a")
end end
end end
end end
describe 'ExternalLinkFilter' do describe 'ExternalLinkFilter' do
let(:links) { get_section('externallinkfilter').next_element }
it 'adds nofollow to external link' do it 'adds nofollow to external link' do
expect(links.css('a').first.to_html).to match 'nofollow' link = doc.at_css('a:contains("Google")')
expect(link.attr('rel')).to match 'nofollow'
end end
it 'ignores internal link' do it 'ignores internal link' do
expect(links.css('a').last.to_html).not_to match 'nofollow' link = doc.at_css('a:contains("GitLab Root")')
expect(link.attr('rel')).not_to match 'nofollow'
end end
end end
describe 'ReferenceFilter' do
it 'handles references in headers' do
header = @doc.at_css('#reference-filters-eg-1').parent
expect(header.css('a').size).to eq 2
end end
it "handles references in Markdown" do context 'default pipeline' do
body = get_section('reference-filters-eg-1') before(:all) do
expect(body).to have_selector('em a.gfm-merge_request', count: 1) @feat = MarkdownFeature.new
end
it 'parses user references' do
body = get_section('userreferencefilter')
expect(body).to have_selector('a.gfm.gfm-project_member', count: 3)
end
it 'parses issue references' do
body = get_section('issuereferencefilter')
expect(body).to have_selector('a.gfm.gfm-issue', count: 2)
end
it 'parses merge request references' do
body = get_section('mergerequestreferencefilter')
expect(body).to have_selector('a.gfm.gfm-merge_request', count: 2)
end
it 'parses snippet references' do
body = get_section('snippetreferencefilter')
expect(body).to have_selector('a.gfm.gfm-snippet', count: 2)
end
it 'parses commit range references' do
body = get_section('commitrangereferencefilter')
expect(body).to have_selector('a.gfm.gfm-commit_range', count: 2)
end
it 'parses commit references' do
body = get_section('commitreferencefilter')
expect(body).to have_selector('a.gfm.gfm-commit', count: 2)
end
it 'parses label references' do
body = get_section('labelreferencefilter')
expect(body).to have_selector('a.gfm.gfm-label', count: 3)
end
end
describe 'Task Lists' do
it 'generates task lists' do
body = get_section('task-lists')
expect(body).to have_selector('ul.task-list', count: 2)
expect(body).to have_selector('li.task-list-item', count: 7)
expect(body).to have_selector('input[checked]', count: 3)
end
end
end
end
# This is a helper class used by the GitLab Markdown feature spec
#
# Because the feature spec only cares about the output of the Markdown, and the
# test setup and teardown and parsing is fairly expensive, we only want to do it
# once. Unfortunately RSpec will not let you access `let`s in a `before(:all)`
# block, so we fake it by encapsulating all the shared setup in this class.
#
# The class renders `spec/fixtures/markdown.md.erb` using ERB, allowing for
# reference to the factory-created objects.
class MarkdownFeature
include FactoryGirl::Syntax::Methods
def initialize
DatabaseCleaner.start
end
def teardown
DatabaseCleaner.clean
end
def user
@user ||= create(:user)
end
def group
unless @group
@group = create(:group)
@group.add_user(user, Gitlab::Access::DEVELOPER)
end
@group
end
# Direct references ----------------------------------------------------------
def project
@project ||= create(:project)
end
def issue
@issue ||= create(:issue, project: project)
end
def merge_request
@merge_request ||= create(:merge_request, :simple, source_project: project)
end
def snippet
@snippet ||= create(:project_snippet, project: project)
end
def commit
@commit ||= project.commit
end
def commit_range # `gfm_with_options` depends on a `@project` variable
unless @commit_range @project = @feat.project
commit2 = project.commit('HEAD~3')
@commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project)
end
@commit_range @html = markdown(@feat.raw_markdown)
end end
def simple_label it_behaves_like 'all pipelines'
@simple_label ||= create(:label, name: 'gfm', project: project)
end
def label it 'includes RelativeLinkFilter' do
@label ||= create(:label, name: 'awaiting feedback', project: project) expect(doc).to parse_relative_links
end end
# Cross-references ----------------------------------------------------------- it 'includes EmojiFilter' do
expect(doc).to parse_emoji
def xproject
unless @xproject
namespace = create(:namespace, name: 'cross-reference')
@xproject = create(:project, namespace: namespace)
@xproject.team << [user, :developer]
end end
@xproject it 'includes TableOfContentsFilter' do
expect(doc).to create_header_links
end end
def xissue it 'includes AutolinkFilter' do
@xissue ||= create(:issue, project: xproject) expect(doc).to create_autolinks
end end
def xmerge_request it 'includes all reference filters' do
@xmerge_request ||= create(:merge_request, :simple, source_project: xproject) aggregate_failures do
expect(doc).to reference_users
expect(doc).to reference_issues
expect(doc).to reference_merge_requests
expect(doc).to reference_snippets
expect(doc).to reference_commit_ranges
expect(doc).to reference_commits
expect(doc).to reference_labels
end end
def xsnippet
@xsnippet ||= create(:project_snippet, project: xproject)
end end
def xcommit it 'includes TaskListFilter' do
@xcommit ||= xproject.commit expect(doc).to parse_task_lists
end end
def xcommit_range
unless @xcommit_range
xcommit2 = xproject.commit('HEAD~2')
@xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject)
end end
@xcommit_range # `markdown` calls these two methods
def current_user
@feat.user
end end
def raw_markdown def user_color_scheme_class
fixture = Rails.root.join('spec/fixtures/markdown.md.erb') :white
ERB.new(File.read(fixture)).result(binding)
end end
end end
...@@ -100,6 +100,13 @@ Markdown should be usable inside a link. Let's try! ...@@ -100,6 +100,13 @@ Markdown should be usable inside a link. Let's try!
- [**text**](#link-strong) - [**text**](#link-strong)
- [`text`](#link-code) - [`text`](#link-code)
### RelativeLinkFilter
Linking to a file relative to this project's repository should work.
[Relative Link](doc/README.md)
![Relative Image](app/assets/images/touch-icon-ipad.png)
### EmojiFilter ### EmojiFilter
Because life would be :zzz: without Emoji, right? :rocket: Because life would be :zzz: without Emoji, right? :rocket:
...@@ -123,9 +130,9 @@ These are all plain text that should get turned into links: ...@@ -123,9 +130,9 @@ These are all plain text that should get turned into links:
But it shouldn't autolink text inside certain tags: But it shouldn't autolink text inside certain tags:
- <code>http://about.gitlab.com/</code> - <code>http://code.gitlab.com/</code>
- <a>http://about.gitlab.com/</a> - <a>http://a.gitlab.com/</a>
- <kbd>http://about.gitlab.com/</kbd> - <kbd>http://kbd.gitlab.com/</kbd>
### ExternalLinkFilter ### ExternalLinkFilter
......
...@@ -6,6 +6,14 @@ describe BlobHelper do ...@@ -6,6 +6,14 @@ describe BlobHelper do
let(:no_context_content) { ":type \"assem\"))" } let(:no_context_content) { ":type \"assem\"))" }
let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" } let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" }
let(:split_content) { blob_content.split("\n") } let(:split_content) { blob_content.split("\n") }
let(:multiline_content) do
%q(
def test(input):
"""This is line 1 of a multi-line comment.
This is line 2.
"""
)
end
it 'should return plaintext for unknown lexer context' do it 'should return plaintext for unknown lexer context' do
result = highlight(blob_name, no_context_content, nowrap: true, continue: false) result = highlight(blob_name, no_context_content, nowrap: true, continue: false)
...@@ -29,5 +37,15 @@ describe BlobHelper do ...@@ -29,5 +37,15 @@ describe BlobHelper do
result = split_content.map{ |content| highlight(blob_name, content, nowrap: true, continue: true) } result = split_content.map{ |content| highlight(blob_name, content, nowrap: true, continue: true) }
expect(result).to eq(expected) expect(result).to eq(expected)
end end
it 'should highlight multi-line comments' do
result = highlight(blob_name, multiline_content, nowrap: true, continue: false)
html = Nokogiri::HTML(result)
lines = html.search('.s')
expect(lines.count).to eq(3)
expect(lines[0].text).to eq('"""This is line 1 of a multi-line comment.')
expect(lines[1].text).to eq(' This is line 2.')
expect(lines[2].text).to eq(' """')
end
end end
end end
# encoding: UTF-8
require 'spec_helper' require 'spec_helper'
module Gitlab::Markdown module Gitlab::Markdown
...@@ -101,6 +103,20 @@ module Gitlab::Markdown ...@@ -101,6 +103,20 @@ module Gitlab::Markdown
expect(doc.at_css('a')['href']).to eq 'http://example.com' expect(doc.at_css('a')['href']).to eq 'http://example.com'
end end
it 'supports Unicode filenames' do
path = 'files/images/한글.png'
escaped = Addressable::URI.escape(path)
# Stub these methods so the file doesn't actually need to be in the repo
allow_any_instance_of(described_class).to receive(:file_exists?).
and_return(true)
allow_any_instance_of(described_class).
to receive(:image?).with(path).and_return(true)
doc = filter(image(escaped))
expect(doc.at_css('img')['src']).to match '/raw/'
end
context 'when requested path is a file in the repo' do context 'when requested path is a file in the repo' do
let(:requested_path) { 'doc/api/README.md' } let(:requested_path) { 'doc/api/README.md' }
include_examples :relative_to_requested include_examples :relative_to_requested
......
...@@ -14,11 +14,14 @@ ...@@ -14,11 +14,14 @@
# default_branch_protection :integer default(2) # default_branch_protection :integer default(2)
# twitter_sharing_enabled :boolean default(TRUE) # twitter_sharing_enabled :boolean default(TRUE)
# restricted_visibility_levels :text # restricted_visibility_levels :text
# version_check_enabled :boolean default(TRUE)
# max_attachment_size :integer default(10), not null # max_attachment_size :integer default(10), not null
# session_expire_delay :integer default(10080), not null
# default_project_visibility :integer # default_project_visibility :integer
# default_snippet_visibility :integer # default_snippet_visibility :integer
# restricted_signup_domains :text # restricted_signup_domains :text
# user_oauth_applications :boolean default(TRUE)
# after_sign_out_path :string(255)
# session_expire_delay :integer default(10080), not null
# #
require 'spec_helper' require 'spec_helper'
......
...@@ -21,12 +21,13 @@ ...@@ -21,12 +21,13 @@
# import_url :string(255) # import_url :string(255)
# visibility_level :integer default(0), not null # visibility_level :integer default(0), not null
# archived :boolean default(FALSE), not null # archived :boolean default(FALSE), not null
# avatar :string(255)
# import_status :string(255) # import_status :string(255)
# repository_size :float default(0.0) # repository_size :float default(0.0)
# star_count :integer default(0), not null # star_count :integer default(0), not null
# import_type :string(255) # import_type :string(255)
# import_source :string(255) # import_source :string(255)
# avatar :string(255) # commit_count :integer default(0)
# #
require 'spec_helper' require 'spec_helper'
...@@ -111,14 +112,20 @@ describe Project do ...@@ -111,14 +112,20 @@ describe Project do
expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git') expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
end end
describe "#web_url" do
let(:project) { create(:empty_project, path: "somewhere") }
it 'returns the full web URL for this repo' do it 'returns the full web URL for this repo' do
project = Project.new(path: 'somewhere') expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.path}/somewhere")
expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/somewhere")
end end
end
describe "#web_url_without_protocol" do
let(:project) { create(:empty_project, path: "somewhere") }
it 'returns the web URL without the protocol for this repo' do it 'returns the web URL without the protocol for this repo' do
project = Project.new(path: 'somewhere') expect(project.web_url_without_protocol).to eq("#{Gitlab.config.gitlab.url.split('://')[1]}/#{project.namespace.path}/somewhere")
expect(project.web_url_without_protocol).to eq("#{Gitlab.config.gitlab.url.split('://')[1]}/somewhere") end
end end
describe 'last_activity methods' do describe 'last_activity methods' do
......
...@@ -57,6 +57,7 @@ ...@@ -57,6 +57,7 @@
# otp_backup_codes :text # otp_backup_codes :text
# public_email :string(255) default(""), not null # public_email :string(255) default(""), not null
# dashboard :integer default(0) # dashboard :integer default(0)
# project_view :integer default(0)
# #
require 'spec_helper' require 'spec_helper'
......
...@@ -6,6 +6,7 @@ describe API::API, api: true do ...@@ -6,6 +6,7 @@ describe API::API, api: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) } let(:key) { create(:key, user: user) }
let(:email) { create(:email, user: user) }
describe "GET /users" do describe "GET /users" do
context "when unauthenticated" do context "when unauthenticated" do
...@@ -384,6 +385,87 @@ describe API::API, api: true do ...@@ -384,6 +385,87 @@ describe API::API, api: true do
end end
end end
describe "POST /users/:id/emails" do
before { admin }
it "should not create invalid email" do
post api("/users/#{user.id}/emails", admin), {}
expect(response.status).to eq(400)
expect(json_response['message']).to eq('400 (Bad request) "email" not given')
end
it "should create email" do
email_attrs = attributes_for :email
expect do
post api("/users/#{user.id}/emails", admin), email_attrs
end.to change{ user.emails.count }.by(1)
end
end
describe 'GET /user/:uid/emails' do
before { admin }
context 'when unauthenticated' do
it 'should return authentication error' do
get api("/users/#{user.id}/emails")
expect(response.status).to eq(401)
end
end
context 'when authenticated' do
it 'should return 404 for non-existing user' do
get api('/users/999999/emails', admin)
expect(response.status).to eq(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'should return array of emails' do
user.emails << email
user.save
get api("/users/#{user.id}/emails", admin)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.first['email']).to eq(email.email)
end
end
end
describe 'DELETE /user/:uid/emails/:id' do
before { admin }
context 'when unauthenticated' do
it 'should return authentication error' do
delete api("/users/#{user.id}/emails/42")
expect(response.status).to eq(401)
end
end
context 'when authenticated' do
it 'should delete existing email' do
user.emails << email
user.save
expect do
delete api("/users/#{user.id}/emails/#{email.id}", admin)
end.to change { user.emails.count }.by(-1)
expect(response.status).to eq(200)
end
it 'should return 404 error if user not found' do
user.emails << email
user.save
delete api("/users/999999/emails/#{email.id}", admin)
expect(response.status).to eq(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'should return 404 error if email not foud' do
delete api("/users/#{user.id}/emails/42", admin)
expect(response.status).to eq(404)
expect(json_response['message']).to eq('404 Email Not Found')
end
end
end
describe "DELETE /users/:id" do describe "DELETE /users/:id" do
before { admin } before { admin }
...@@ -528,6 +610,95 @@ describe API::API, api: true do ...@@ -528,6 +610,95 @@ describe API::API, api: true do
end end
end end
describe "GET /user/emails" do
context "when unauthenticated" do
it "should return authentication error" do
get api("/user/emails")
expect(response.status).to eq(401)
end
end
context "when authenticated" do
it "should return array of emails" do
user.emails << email
user.save
get api("/user/emails", user)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.first["email"]).to eq(email.email)
end
end
end
describe "GET /user/emails/:id" do
it "should return single email" do
user.emails << email
user.save
get api("/user/emails/#{email.id}", user)
expect(response.status).to eq(200)
expect(json_response["email"]).to eq(email.email)
end
it "should return 404 Not Found within invalid ID" do
get api("/user/emails/42", user)
expect(response.status).to eq(404)
expect(json_response['message']).to eq('404 Not found')
end
it "should return 404 error if admin accesses user's email" do
user.emails << email
user.save
admin
get api("/user/emails/#{email.id}", admin)
expect(response.status).to eq(404)
expect(json_response['message']).to eq('404 Not found')
end
end
describe "POST /user/emails" do
it "should create email" do
email_attrs = attributes_for :email
expect do
post api("/user/emails", user), email_attrs
end.to change{ user.emails.count }.by(1)
expect(response.status).to eq(201)
end
it "should return a 401 error if unauthorized" do
post api("/user/emails"), email: 'some email'
expect(response.status).to eq(401)
end
it "should not create email with invalid email" do
post api("/user/emails", user), {}
expect(response.status).to eq(400)
expect(json_response['message']).to eq('400 (Bad request) "email" not given')
end
end
describe "DELETE /user/emails/:id" do
it "should delete existed email" do
user.emails << email
user.save
expect do
delete api("/user/emails/#{email.id}", user)
end.to change{user.emails.count}.by(-1)
expect(response.status).to eq(200)
end
it "should return success if email ID not found" do
delete api("/user/emails/42", user)
expect(response.status).to eq(200)
end
it "should return 401 error if unauthorized" do
user.emails << email
user.save
delete api("/user/emails/#{email.id}")
expect(response.status).to eq(401)
end
end
describe 'PUT /user/:id/block' do describe 'PUT /user/:id/block' do
before { admin } before { admin }
it 'should block existing user' do it 'should block existing user' do
......
...@@ -29,7 +29,7 @@ describe Projects::ForkService do ...@@ -29,7 +29,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("Failed to fork repository") expect(@to_project.errors[:base]).to include("Failed to fork repository via gitlab-shell")
end end
end end
......
# This is a helper class used by the GitLab Markdown feature spec
#
# Because the feature spec only cares about the output of the Markdown, and the
# test setup and teardown and parsing is fairly expensive, we only want to do it
# once. Unfortunately RSpec will not let you access `let`s in a `before(:all)`
# block, so we fake it by encapsulating all the shared setup in this class.
#
# The class renders `spec/fixtures/markdown.md.erb` using ERB, allowing for
# reference to the factory-created objects.
class MarkdownFeature
include FactoryGirl::Syntax::Methods
def user
@user ||= create(:user)
end
def group
unless @group
@group = create(:group)
@group.add_user(user, Gitlab::Access::DEVELOPER)
end
@group
end
# Direct references ----------------------------------------------------------
def project
@project ||= create(:project)
end
def issue
@issue ||= create(:issue, project: project)
end
def merge_request
@merge_request ||= create(:merge_request, :simple, source_project: project)
end
def snippet
@snippet ||= create(:project_snippet, project: project)
end
def commit
@commit ||= project.commit
end
def commit_range
unless @commit_range
commit2 = project.commit('HEAD~3')
@commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project)
end
@commit_range
end
def simple_label
@simple_label ||= create(:label, name: 'gfm', project: project)
end
def label
@label ||= create(:label, name: 'awaiting feedback', project: project)
end
# Cross-references -----------------------------------------------------------
def xproject
unless @xproject
namespace = create(:namespace, name: 'cross-reference')
@xproject = create(:project, namespace: namespace)
@xproject.team << [user, :developer]
end
@xproject
end
def xissue
@xissue ||= create(:issue, project: xproject)
end
def xmerge_request
@xmerge_request ||= create(:merge_request, :simple, source_project: xproject)
end
def xsnippet
@xsnippet ||= create(:project_snippet, project: xproject)
end
def xcommit
@xcommit ||= xproject.commit
end
def xcommit_range
unless @xcommit_range
xcommit2 = xproject.commit('HEAD~2')
@xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject)
end
@xcommit_range
end
def raw_markdown
fixture = Rails.root.join('spec/fixtures/markdown.md.erb')
ERB.new(File.read(fixture)).result(binding)
end
end
# MarkdownMatchers
#
# Custom matchers for our custom HTML::Pipeline filters. These are used to test
# that specific filters are or are not used by our defined pipelines.
#
# Must be included manually.
module MarkdownMatchers
extend RSpec::Matchers::DSL
include Capybara::Node::Matchers
# RelativeLinkFilter
matcher :parse_relative_links do
set_default_markdown_messages
match do |actual|
link = actual.at_css('a:contains("Relative Link")')
image = actual.at_css('img[alt="Relative Image"]')
expect(link['href']).to end_with('master/doc/README.md')
expect(image['src']).to end_with('master/app/assets/images/touch-icon-ipad.png')
end
end
# EmojiFilter
matcher :parse_emoji do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('img.emoji', count: 10)
end
end
# TableOfContentsFilter
matcher :create_header_links do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('h1 a#gitlab-markdown')
expect(actual).to have_selector('h2 a#markdown')
expect(actual).to have_selector('h3 a#autolinkfilter')
end
end
# AutolinkFilter
matcher :create_autolinks do
def have_autolink(link)
have_link(link, href: link)
end
set_default_markdown_messages
match do |actual|
expect(actual).to have_autolink('http://about.gitlab.com/')
expect(actual).to have_autolink('https://google.com/')
expect(actual).to have_autolink('ftp://ftp.us.debian.org/debian/')
expect(actual).to have_autolink('smb://foo/bar/baz')
expect(actual).to have_autolink('irc://irc.freenode.net/git')
expect(actual).to have_autolink('http://localhost:3000')
%w(code a kbd).each do |elem|
expect(body).not_to have_selector("#{elem} a")
end
end
end
# UserReferenceFilter
matcher :reference_users do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('a.gfm.gfm-project_member', count: 3)
end
end
# IssueReferenceFilter
matcher :reference_issues do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('a.gfm.gfm-issue', count: 3)
end
end
# MergeRequestReferenceFilter
matcher :reference_merge_requests do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('a.gfm.gfm-merge_request', count: 3)
expect(actual).to have_selector('em a.gfm-merge_request')
end
end
# SnippetReferenceFilter
matcher :reference_snippets do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('a.gfm.gfm-snippet', count: 2)
end
end
# CommitRangeReferenceFilter
matcher :reference_commit_ranges do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('a.gfm.gfm-commit_range', count: 2)
end
end
# CommitReferenceFilter
matcher :reference_commits do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('a.gfm.gfm-commit', count: 2)
end
end
# LabelReferenceFilter
matcher :reference_labels do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('a.gfm.gfm-label', count: 3)
end
end
# TaskListFilter
matcher :parse_task_lists do
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('ul.task-list', count: 2)
expect(actual).to have_selector('li.task-list-item', count: 7)
expect(actual).to have_selector('input[checked]', count: 3)
end
end
end
# Monkeypatch the matcher DSL so that we can reduce some noisy duplication for
# setting the failure messages for these matchers
module RSpec::Matchers::DSL::Macros
def set_default_markdown_messages
failure_message do
# expected to parse emoji, but didn't
"expected to #{description}, but didn't"
end
failure_message_when_negated do
# expected not to parse task lists, but did
"expected not to #{description}, but did"
end
end
end
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