Merge branch 'master' of github.com:gitlabhq/gitlabhq
This commit is contained in:
commit
b118f648cb
24
CHANGELOG
24
CHANGELOG
|
@ -1,11 +1,13 @@
|
|||
Please view this file on the master branch, on stable branches it's out of date.
|
||||
|
||||
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)
|
||||
- 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 commit data retrieval when branch name has single quotes (Stan Hu)
|
||||
- Fix Error 500 when browsing projects with no HEAD (Stan Hu)
|
||||
- Add rake task 'gitlab:update_commit_count' (Daniel Gerhardt)
|
||||
- Check that project was actually created rather than just validated in import:repos task (Stan Hu)
|
||||
- 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 the "Reload with full diff" URL button (Stan Hu)
|
||||
|
@ -18,6 +20,23 @@ v 7.14.0 (unreleased)
|
|||
- Add support for destroying project milestones (Stan Hu)
|
||||
- Add fetch command to the MR page.
|
||||
- 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
|
||||
- Fix: Label modifications are not reflected in existing notes and in the issue list
|
||||
|
@ -28,6 +47,7 @@ v 7.13.1
|
|||
- Fix: ActionView::Template::Error
|
||||
- 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.
|
||||
- Render Note field hints consistently for "new" and "edit" forms
|
||||
|
||||
v 7.13.0
|
||||
- Remove repository graph log to fix slow cache updates after push event (Stan Hu)
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
font-family: $monospace_font;
|
||||
white-space: pre;
|
||||
word-wrap: normal;
|
||||
padding: 0;
|
||||
padding: 1px 2px;
|
||||
}
|
||||
|
||||
kbd {
|
||||
|
|
|
@ -38,6 +38,10 @@ code {
|
|||
}
|
||||
}
|
||||
|
||||
a > code {
|
||||
color: $link-color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wiki typography
|
||||
*
|
||||
|
|
|
@ -18,4 +18,10 @@ class Groups::ApplicationController < ApplicationController
|
|||
return render_404
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_admin_group_member!
|
||||
unless can?(current_user, :admin_group_member, group)
|
||||
return render_403
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
|
|||
# Authorize
|
||||
before_action :authorize_read_group!
|
||||
before_action :authorize_admin_group!, except: [:index, :leave]
|
||||
before_action :authorize_admin_group_member!, only: [:create, :resend_invite]
|
||||
|
||||
def index
|
||||
@project = @group.projects.find(params[:project_id]) if params[:project_id]
|
||||
|
@ -28,6 +29,9 @@ class Groups::GroupMembersController < Groups::ApplicationController
|
|||
|
||||
def update
|
||||
@member = @group.group_members.find(params[:id])
|
||||
|
||||
return render_403 unless can?(current_user, :update_group_member, @member)
|
||||
|
||||
@member.update_attributes(member_params)
|
||||
end
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ class GroupsController < Groups::ApplicationController
|
|||
|
||||
if @group.save
|
||||
@group.add_owner(current_user)
|
||||
redirect_to @group, notice: 'Group was successfully created.'
|
||||
redirect_to @group, notice: "Group '#{@group.name}' was successfully created."
|
||||
else
|
||||
render action: "new"
|
||||
end
|
||||
|
@ -75,7 +75,7 @@ class GroupsController < Groups::ApplicationController
|
|||
|
||||
def update
|
||||
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
|
||||
render action: "edit"
|
||||
end
|
||||
|
@ -84,7 +84,7 @@ class GroupsController < Groups::ApplicationController
|
|||
def destroy
|
||||
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
|
||||
|
||||
protected
|
||||
|
|
|
@ -7,6 +7,10 @@ class Projects::NetworkController < Projects::ApplicationController
|
|||
before_action :authorize_download_code!
|
||||
|
||||
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|
|
||||
format.html
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ class ProjectsController < ApplicationController
|
|||
if @project.saved?
|
||||
redirect_to(
|
||||
project_path(@project),
|
||||
notice: 'Project was successfully created.'
|
||||
notice: "Project '#{@project.name}' was successfully created."
|
||||
)
|
||||
else
|
||||
render 'new'
|
||||
|
@ -36,11 +36,11 @@ class ProjectsController < ApplicationController
|
|||
|
||||
respond_to do |format|
|
||||
if status
|
||||
flash[:notice] = 'Project was successfully updated.'
|
||||
flash[:notice] = "Project '#{@project.name}' was successfully updated."
|
||||
format.html do
|
||||
redirect_to(
|
||||
edit_project_path(@project),
|
||||
notice: 'Project was successfully updated.'
|
||||
notice: "Project '#{@project.name}' was successfully updated."
|
||||
)
|
||||
end
|
||||
format.js
|
||||
|
@ -100,7 +100,7 @@ class ProjectsController < ApplicationController
|
|||
return access_denied! unless can?(current_user, :remove_project, @project)
|
||||
|
||||
::Projects::DestroyService.new(@project, current_user, {}).execute
|
||||
flash[:alert] = 'Project deleted.'
|
||||
flash[:alert] = "Project '#{@project.name}' was deleted."
|
||||
|
||||
if request.referer.include?('/admin')
|
||||
redirect_to admin_namespaces_projects_path
|
||||
|
|
|
@ -233,7 +233,8 @@ class Ability
|
|||
if group.has_owner?(user) || user.admin?
|
||||
rules.push(*[
|
||||
:admin_group,
|
||||
:admin_namespace
|
||||
:admin_namespace,
|
||||
:admin_group_member
|
||||
])
|
||||
end
|
||||
|
||||
|
@ -295,7 +296,7 @@ class Ability
|
|||
rules = []
|
||||
target_user = subject.user
|
||||
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)
|
||||
rules << :update_group_member
|
||||
|
|
|
@ -14,13 +14,14 @@
|
|||
# default_branch_protection :integer default(2)
|
||||
# twitter_sharing_enabled :boolean default(TRUE)
|
||||
# restricted_visibility_levels :text
|
||||
# version_check_enabled :boolean default(TRUE)
|
||||
# max_attachment_size :integer default(10), not null
|
||||
# session_expire_delay :integer default(10080), not null
|
||||
# default_project_visibility :integer
|
||||
# default_snippet_visibility :integer
|
||||
# restricted_signup_domains :text
|
||||
# user_oauth_applications :bool default(TRUE)
|
||||
# user_oauth_applications :boolean default(TRUE)
|
||||
# after_sign_out_path :string(255)
|
||||
# session_expire_delay :integer default(10080), not null
|
||||
#
|
||||
|
||||
class ApplicationSetting < ActiveRecord::Base
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
# == 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
|
||||
serialize :details, Hash
|
||||
|
||||
|
|
|
@ -56,6 +56,12 @@ class Group < Namespace
|
|||
name
|
||||
end
|
||||
|
||||
def avatar_url(size = nil)
|
||||
if avatar.present?
|
||||
[gitlab_config.url, avatar.url].join
|
||||
end
|
||||
end
|
||||
|
||||
def owners
|
||||
@owners ||= group_members.owners.map(&:user)
|
||||
end
|
||||
|
|
|
@ -21,12 +21,13 @@
|
|||
# import_url :string(255)
|
||||
# visibility_level :integer default(0), not null
|
||||
# archived :boolean default(FALSE), not null
|
||||
# avatar :string(255)
|
||||
# import_status :string(255)
|
||||
# repository_size :float default(0.0)
|
||||
# star_count :integer default(0), not null
|
||||
# import_type :string(255)
|
||||
# import_source :string(255)
|
||||
# avatar :string(255)
|
||||
# commit_count :integer default(0)
|
||||
#
|
||||
|
||||
require 'carrierwave/orm/activerecord'
|
||||
|
@ -36,7 +37,6 @@ class Project < ActiveRecord::Base
|
|||
include Gitlab::ConfigHelper
|
||||
include Gitlab::ShellAdapter
|
||||
include Gitlab::VisibilityLevel
|
||||
include Rails.application.routes.url_helpers
|
||||
include Referable
|
||||
include Sortable
|
||||
|
||||
|
@ -316,7 +316,7 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def web_url
|
||||
[gitlab_config.url, path_with_namespace].join('/')
|
||||
Rails.application.routes.url_helpers.namespace_project_url(self.namespace, self)
|
||||
end
|
||||
|
||||
def web_url_without_protocol
|
||||
|
@ -433,7 +433,7 @@ class Project < ActiveRecord::Base
|
|||
if avatar.present?
|
||||
[gitlab_config.url, avatar.url].join
|
||||
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
|
||||
|
||||
|
@ -571,7 +571,7 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def http_url_to_repo
|
||||
[gitlab_config.url, '/', path_with_namespace, '.git'].join('')
|
||||
"#{web_url}.git"
|
||||
end
|
||||
|
||||
# Check if current branch name is marked as protected in the system
|
||||
|
@ -705,14 +705,14 @@ class Project < ActiveRecord::Base
|
|||
ensure_satellite_exists
|
||||
true
|
||||
else
|
||||
errors.add(:base, 'Failed to fork repository')
|
||||
errors.add(:base, 'Failed to fork repository via gitlab-shell')
|
||||
false
|
||||
end
|
||||
else
|
||||
if gitlab_shell.add_repository(path_with_namespace)
|
||||
true
|
||||
else
|
||||
errors.add(:base, 'Failed to create repository')
|
||||
errors.add(:base, 'Failed to create repository via gitlab-shell')
|
||||
false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,2 +1,16 @@
|
|||
# == 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
|
||||
end
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
# otp_backup_codes :text
|
||||
# public_email :string(255) default(""), not null
|
||||
# dashboard :integer default(0)
|
||||
# project_view :integer default(0)
|
||||
#
|
||||
|
||||
require 'carrierwave/orm/activerecord'
|
||||
|
|
|
@ -51,21 +51,22 @@
|
|||
= paginate @projects, param_name: 'projects_page', theme: 'gitlab'
|
||||
|
||||
.col-md-6
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
Add user(s) to the group:
|
||||
.panel-body.form-holder
|
||||
%p.light
|
||||
Read more about project permissions
|
||||
%strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
|
||||
- if can?(current_user, :admin_group_member, @group)
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
Add user(s) to the group:
|
||||
.panel-body.form-holder
|
||||
%p.light
|
||||
Read more about project permissions
|
||||
%strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
|
||||
|
||||
= form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
|
||||
%div
|
||||
= users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all)
|
||||
%div.prepend-top-10
|
||||
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
|
||||
%hr
|
||||
= button_tag 'Add users to group', class: "btn btn-create"
|
||||
= form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
|
||||
%div
|
||||
= users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all)
|
||||
%div.prepend-top-10
|
||||
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
|
||||
%hr
|
||||
= button_tag 'Add users to group', class: "btn btn-create"
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
%h3.panel-title
|
||||
|
@ -86,7 +87,8 @@
|
|||
(invited)
|
||||
%span.pull-right.light
|
||||
= member.human_access
|
||||
= 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
|
||||
- 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
|
||||
%i.fa.fa-minus.fa-inverse
|
||||
.panel-footer
|
||||
= paginate @members, param_name: 'members_page', theme: 'gitlab'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
= 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"
|
||||
- if devise_mapping.rememberable?
|
||||
.remember-me.checkbox
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
= link_to member.created_by.name, user_path(member.created_by)
|
||||
= 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
|
||||
Resend invite
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
= search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input' }
|
||||
= 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
|
||||
= button_tag class: 'btn btn-new js-toggle-button', type: 'button' do
|
||||
Add members
|
||||
|
|
|
@ -6,14 +6,13 @@
|
|||
- @key.errors.full_messages.each do |msg|
|
||||
%li= msg
|
||||
|
||||
.form-group
|
||||
= f.label :title, class: 'control-label'
|
||||
.col-sm-10= f.text_field :title, class: "form-control"
|
||||
.form-group
|
||||
= f.label :key, class: 'control-label'
|
||||
.col-sm-10
|
||||
= 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
|
||||
= f.submit 'Add key', class: "btn btn-create"
|
||||
|
|
|
@ -17,9 +17,9 @@
|
|||
|
||||
:javascript
|
||||
network_graph = new Network({
|
||||
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")}',
|
||||
ref: '#{@ref}',
|
||||
url: "#{escape_javascript(@url)}",
|
||||
commit_url: "#{escape_javascript(@commit_url)}",
|
||||
ref: "#{escape_javascript(@ref)}",
|
||||
commit_id: '#{@commit.id}'
|
||||
})
|
||||
new ShortcutsNetwork(network_graph.branch_graph)
|
||||
|
|
132
doc/api/users.md
132
doc/api/users.md
|
@ -397,6 +397,138 @@ Parameters:
|
|||
|
||||
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
|
||||
|
||||
Blocks the specified user. Available only for admin.
|
||||
|
|
|
@ -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.
|
||||
|
||||
1. Sign in to [Twitter Developers](https://dev.twitter.com/) area.
|
||||
|
||||
1. Hover over the avatar in the top right corner and select "My applications."
|
||||
1. Sign in to [Twitter Application Management](https://apps.twitter.com/).
|
||||
|
||||
1. Select "Create new app"
|
||||
|
||||
|
@ -14,18 +12,18 @@ To enable the Twitter OmniAuth provider you must register your application with
|
|||
- Description: Create a description.
|
||||
- Website: The URL to your GitLab installation. 'https://gitlab.example.com'
|
||||
- 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)
|
||||
1. Select "Create your Twitter application."
|
||||
|
||||
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 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.
|
||||
|
||||
|
@ -78,4 +76,4 @@ To enable the Twitter OmniAuth provider you must register your application with
|
|||
|
||||
1. Restart GitLab for the changes to take effect.
|
||||
|
||||
On the sign in page there should now be a Twitter icon below the regular sign in form. Click the icon to begin the authentication process. Twitter will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
|
||||
On the sign in page there should now be a Twitter icon below the regular sign in form. Click the icon to begin the authentication process. Twitter will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
|
|
@ -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 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.
|
||||
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.
|
|||
|
||||
![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 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.
|
||||
|
@ -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.
|
||||
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.
|
||||
|
||||
## Issues with GitLab flow
|
||||
|
@ -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.
|
||||
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/).
|
||||
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.
|
||||
|
||||
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
|
|||
And page should select "master" in select box
|
||||
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
|
||||
Scenario: I should switch "branch" and "tag"
|
||||
When I switch ref to "feature"
|
||||
|
|
|
@ -11,8 +11,12 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
|
|||
# Stub Graph max_size to speed up test (10 commits vs. 650)
|
||||
Network::Graph.stub(max_count: 10)
|
||||
|
||||
project = Project.find_by(name: "Shop")
|
||||
visit namespace_project_network_path(project.namespace, project, "master")
|
||||
@project = Project.find_by(name: "Shop")
|
||||
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
|
||||
|
||||
step 'page should select "master" in select box' do
|
||||
|
@ -29,6 +33,12 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
|
|||
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
|
||||
select 'feature', from: 'ref'
|
||||
sleep 2
|
||||
|
|
|
@ -6,6 +6,10 @@ module API
|
|||
|
||||
class UserBasic < UserSafe
|
||||
expose :id, :state, :avatar_url
|
||||
|
||||
expose :web_url do |user, options|
|
||||
Rails.application.routes.url_helpers.user_url(user)
|
||||
end
|
||||
end
|
||||
|
||||
class User < UserBasic
|
||||
|
@ -31,6 +35,10 @@ module API
|
|||
expose :private_token
|
||||
end
|
||||
|
||||
class Email < Grape::Entity
|
||||
expose :id, :email
|
||||
end
|
||||
|
||||
class Hook < Grape::Entity
|
||||
expose :id, :url, :created_at
|
||||
end
|
||||
|
@ -59,6 +67,7 @@ module API
|
|||
expose :namespace
|
||||
expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? }
|
||||
expose :avatar_url
|
||||
expose :star_count, :forks_count
|
||||
end
|
||||
|
||||
class ProjectMember < UserBasic
|
||||
|
@ -69,6 +78,11 @@ module API
|
|||
|
||||
class Group < Grape::Entity
|
||||
expose :id, :name, :path, :description
|
||||
expose :avatar_url
|
||||
|
||||
expose :web_url do |group, options|
|
||||
Rails.application.routes.url_helpers.group_url(group)
|
||||
end
|
||||
end
|
||||
|
||||
class GroupDetail < Group
|
||||
|
|
123
lib/api/users.rb
123
lib/api/users.rb
|
@ -131,11 +131,11 @@ module API
|
|||
# Add ssh key to a specified user. Only available to admin users.
|
||||
#
|
||||
# Parameters:
|
||||
# id (required) - The ID of a user
|
||||
# key (required) - New SSH Key
|
||||
# title (required) - New SSH Key's title
|
||||
# id (required) - The ID of a user
|
||||
# key (required) - New SSH Key
|
||||
# title (required) - New SSH Key's title
|
||||
# Example Request:
|
||||
# POST /users/:id/keys
|
||||
# POST /users/:id/keys
|
||||
post ":id/keys" do
|
||||
authenticated_as_admin!
|
||||
required_attributes! [:title, :key]
|
||||
|
@ -153,9 +153,9 @@ module API
|
|||
# Get ssh keys of a specified user. Only available to admin users.
|
||||
#
|
||||
# Parameters:
|
||||
# uid (required) - The ID of a user
|
||||
# uid (required) - The ID of a user
|
||||
# Example Request:
|
||||
# GET /users/:uid/keys
|
||||
# GET /users/:uid/keys
|
||||
get ':uid/keys' do
|
||||
authenticated_as_admin!
|
||||
user = User.find_by(id: params[:uid])
|
||||
|
@ -185,6 +185,65 @@ module API
|
|||
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
|
||||
#
|
||||
# Example Request:
|
||||
|
@ -289,6 +348,58 @@ module API
|
|||
rescue
|
||||
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
|
||||
|
|
|
@ -7,7 +7,11 @@ module Backup
|
|||
def initialize
|
||||
@config = YAML.load_file(File.join(Rails.root,'config','database.yml'))[Rails.env]
|
||||
@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
|
||||
|
||||
def dump
|
||||
|
@ -25,7 +29,6 @@ module Backup
|
|||
abort 'Backup failed' unless success
|
||||
|
||||
$progress.print 'Compressing database ... '
|
||||
FileUtils.rm_f db_file_name_gz
|
||||
success = system('gzip', db_file_name)
|
||||
report_success(success)
|
||||
abort 'Backup failed: compress error' unless success
|
||||
|
|
|
@ -16,8 +16,6 @@ module Backup
|
|||
file << s.to_yaml.gsub(/^---\n/,'')
|
||||
end
|
||||
|
||||
FileUtils.chmod(0700, folders_to_backup)
|
||||
|
||||
# create archive
|
||||
$progress.print "Creating backup archive: #{tar_file} ... "
|
||||
# Set file permissions on open to prevent chmod races.
|
||||
|
|
|
@ -130,7 +130,10 @@ module Backup
|
|||
|
||||
def prepare
|
||||
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
|
||||
|
||||
def silent
|
||||
|
|
|
@ -10,7 +10,11 @@ module Backup
|
|||
|
||||
# Copy uploads from public/uploads to backup/uploads
|
||||
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)
|
||||
end
|
||||
|
||||
|
|
|
@ -98,15 +98,25 @@ module Gitlab
|
|||
#
|
||||
# Returns a String
|
||||
def path_type(path)
|
||||
if repository.tree(current_sha, path).entries.any?
|
||||
unescaped_path = Addressable::URI.unescape(path)
|
||||
|
||||
if tree?(unescaped_path)
|
||||
'tree'
|
||||
elsif repository.blob_at(current_sha, path).try(:image?)
|
||||
elsif image?(unescaped_path)
|
||||
'raw'
|
||||
else
|
||||
'blob'
|
||||
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
|
||||
context[:commit].try(:id) ||
|
||||
ref ? repository.commit(ref).try(:sha) : repository.head_commit.sha
|
||||
|
|
|
@ -148,6 +148,12 @@ module Rouge
|
|||
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)
|
||||
# http://stackoverflow.com/a/1600584/2587286
|
||||
val = CGI.escapeHTML(val)
|
||||
|
@ -155,11 +161,13 @@ module Rouge
|
|||
if tok.shortname.empty?
|
||||
val
|
||||
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
|
||||
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
|
||||
"<span class=\"#{tok.shortname}\">#{val}</span>"
|
||||
wrap_values(val, "class=\"#{tok.shortname}\"")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -485,7 +485,8 @@ namespace :gitlab do
|
|||
|
||||
if project.empty_repo?
|
||||
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
|
||||
else
|
||||
puts "wrong or missing hooks".red
|
||||
|
@ -754,7 +755,7 @@ namespace :gitlab do
|
|||
print "Ruby version >= #{required_version} ? ... "
|
||||
|
||||
if current_version.valid? && required_version <= current_version
|
||||
puts "yes (#{current_version})".green
|
||||
puts "yes (#{current_version})".green
|
||||
else
|
||||
puts "no".red
|
||||
try_fixing_it(
|
||||
|
@ -772,7 +773,7 @@ namespace :gitlab do
|
|||
print "Git version >= #{required_version} ? ... "
|
||||
|
||||
if current_version.valid? && required_version <= current_version
|
||||
puts "yes (#{current_version})".green
|
||||
puts "yes (#{current_version})".green
|
||||
else
|
||||
puts "no".red
|
||||
try_fixing_it(
|
||||
|
@ -806,4 +807,3 @@ namespace :gitlab do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -62,11 +62,11 @@ namespace :gitlab do
|
|||
|
||||
project = Projects::CreateService.new(user, project_params).execute
|
||||
|
||||
if project.valid?
|
||||
if project.persisted?
|
||||
puts " * Created #{project.name} (#{repo_path})".green
|
||||
else
|
||||
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
|
||||
|
|
|
@ -21,12 +21,13 @@
|
|||
# import_url :string(255)
|
||||
# visibility_level :integer default(0), not null
|
||||
# archived :boolean default(FALSE), not null
|
||||
# avatar :string(255)
|
||||
# import_status :string(255)
|
||||
# repository_size :float default(0.0)
|
||||
# star_count :integer default(0), not null
|
||||
# import_type :string(255)
|
||||
# import_source :string(255)
|
||||
# avatar :string(255)
|
||||
# commit_count :integer default(0)
|
||||
#
|
||||
|
||||
FactoryGirl.define do
|
||||
|
|
|
@ -17,26 +17,208 @@ require 'erb'
|
|||
# -> Post-process HTML
|
||||
# -> `gfm_with_options` helper
|
||||
# -> HTML::Pipeline
|
||||
# -> Sanitize
|
||||
# -> RelativeLink
|
||||
# -> Emoji
|
||||
# -> Table of Contents
|
||||
# -> Autolinks
|
||||
# -> Rinku (http, https, ftp)
|
||||
# -> Other schemes
|
||||
# -> ExternalLink
|
||||
# -> References
|
||||
# -> TaskList
|
||||
# -> SanitizationFilter
|
||||
# -> Other filters, depending on pipeline
|
||||
# -> `html_safe`
|
||||
# -> Template
|
||||
#
|
||||
# See the MarkdownFeature class for setup details.
|
||||
|
||||
describe 'GitLab Markdown', feature: true do
|
||||
include ActionView::Helpers::TagHelper
|
||||
include ActionView::Helpers::UrlHelper
|
||||
include Capybara::Node::Matchers
|
||||
include GitlabMarkdownHelper
|
||||
include MarkdownMatchers
|
||||
|
||||
# Sometimes it can be useful to see the parsed output of the Markdown document
|
||||
# for debugging. Call this method to write the output to
|
||||
# `tmp/capybara/<filename>.html`.
|
||||
def write_markdown(filename = 'markdown_spec')
|
||||
File.open(Rails.root.join("tmp/capybara/#{filename}.html"), 'w') do |file|
|
||||
file.puts @html
|
||||
end
|
||||
end
|
||||
|
||||
def doc(html = @html)
|
||||
Nokogiri::HTML::DocumentFragment.parse(html)
|
||||
end
|
||||
|
||||
# Shared behavior that all pipelines should exhibit
|
||||
shared_examples 'all pipelines' do
|
||||
describe 'Redcarpet extensions' do
|
||||
it 'does not parse emphasis inside of words' do
|
||||
expect(doc.to_html).not_to match('foo<em>bar</em>baz')
|
||||
end
|
||||
|
||||
it 'parses table Markdown' do
|
||||
aggregate_failures do
|
||||
expect(doc).to have_selector('th:contains("Header")')
|
||||
expect(doc).to have_selector('th:contains("Row")')
|
||||
expect(doc).to have_selector('th:contains("Example")')
|
||||
end
|
||||
end
|
||||
|
||||
it 'allows Markdown in tables' do
|
||||
expect(doc.at_css('td:contains("Baz")').children.to_html).
|
||||
to eq '<strong>Baz</strong>'
|
||||
end
|
||||
|
||||
it 'parses fenced code blocks' do
|
||||
aggregate_failures do
|
||||
expect(doc).to have_selector('pre.code.highlight.white.c')
|
||||
expect(doc).to have_selector('pre.code.highlight.white.python')
|
||||
end
|
||||
end
|
||||
|
||||
it 'parses strikethroughs' do
|
||||
expect(doc).to have_selector(%{del:contains("and this text doesn't")})
|
||||
end
|
||||
|
||||
it 'parses superscript' do
|
||||
expect(doc).to have_selector('sup', count: 2)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'SanitizationFilter' do
|
||||
it 'permits b elements' do
|
||||
expect(doc).to have_selector('b:contains("b tag")')
|
||||
end
|
||||
|
||||
it 'permits em elements' do
|
||||
expect(doc).to have_selector('em:contains("em tag")')
|
||||
end
|
||||
|
||||
it 'permits code elements' do
|
||||
expect(doc).to have_selector('code:contains("code tag")')
|
||||
end
|
||||
|
||||
it 'permits kbd elements' do
|
||||
expect(doc).to have_selector('kbd:contains("s")')
|
||||
end
|
||||
|
||||
it 'permits strike elements' do
|
||||
expect(doc).to have_selector('strike:contains(Emoji)')
|
||||
end
|
||||
|
||||
it 'permits img elements' do
|
||||
expect(doc).to have_selector('img[src*="smile.png"]')
|
||||
end
|
||||
|
||||
it 'permits br elements' do
|
||||
expect(doc).to have_selector('br')
|
||||
end
|
||||
|
||||
it 'permits hr elements' do
|
||||
expect(doc).to have_selector('hr')
|
||||
end
|
||||
|
||||
it 'permits span elements' do
|
||||
expect(doc).to have_selector('span:contains("span tag")')
|
||||
end
|
||||
|
||||
it 'permits style attribute in th elements' do
|
||||
aggregate_failures do
|
||||
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
|
||||
|
||||
it 'permits style attribute in td elements' do
|
||||
aggregate_failures do
|
||||
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
|
||||
expect(doc).not_to have_selector('a[rel="bookmark"]')
|
||||
end
|
||||
|
||||
it "removes `href` from `a` elements if it's fishy" do
|
||||
expect(doc).not_to have_selector('a[href*="javascript"]')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Escaping' do
|
||||
it 'escapes non-tag angle brackets' do
|
||||
table = doc.css('table').last.at_css('tbody')
|
||||
expect(table.at_xpath('.//tr[1]/td[3]').inner_html).to eq '1 < 3 & 5'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Edge Cases' do
|
||||
it 'allows markup inside link elements' do
|
||||
aggregate_failures do
|
||||
expect(doc.at_css('a[href="#link-emphasis"]').to_html).
|
||||
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).
|
||||
to eq %{<a href="#link-code"><code>text</code></a>}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'ExternalLinkFilter' do
|
||||
it 'adds nofollow to external link' do
|
||||
link = doc.at_css('a:contains("Google")')
|
||||
expect(link.attr('rel')).to match 'nofollow'
|
||||
end
|
||||
|
||||
it 'ignores internal link' do
|
||||
link = doc.at_css('a:contains("GitLab Root")')
|
||||
expect(link.attr('rel')).not_to match 'nofollow'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'default pipeline' do
|
||||
before(:all) do
|
||||
@feat = MarkdownFeature.new
|
||||
|
||||
# `gfm_with_options` depends on a `@project` variable
|
||||
@project = @feat.project
|
||||
|
||||
@html = markdown(@feat.raw_markdown)
|
||||
end
|
||||
|
||||
it_behaves_like 'all pipelines'
|
||||
|
||||
it 'includes RelativeLinkFilter' do
|
||||
expect(doc).to parse_relative_links
|
||||
end
|
||||
|
||||
it 'includes EmojiFilter' do
|
||||
expect(doc).to parse_emoji
|
||||
end
|
||||
|
||||
it 'includes TableOfContentsFilter' do
|
||||
expect(doc).to create_header_links
|
||||
end
|
||||
|
||||
it 'includes AutolinkFilter' do
|
||||
expect(doc).to create_autolinks
|
||||
end
|
||||
|
||||
it 'includes all reference filters' do
|
||||
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
|
||||
|
||||
it 'includes TaskListFilter' do
|
||||
expect(doc).to parse_task_lists
|
||||
end
|
||||
end
|
||||
|
||||
# `markdown` calls these two methods
|
||||
def current_user
|
||||
|
@ -46,381 +228,4 @@ describe 'GitLab Markdown', feature: true do
|
|||
def user_color_scheme_class
|
||||
: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
|
||||
|
||||
after(:all) do
|
||||
@feat.teardown
|
||||
end
|
||||
|
||||
# Given a header ID, goes to that element's parent (the header itself), then
|
||||
# its next sibling element (the body).
|
||||
def get_section(id)
|
||||
@doc.at_css("##{id}").parent.next_element
|
||||
end
|
||||
|
||||
# Sometimes it can be useful to see the parsed output of the Markdown document
|
||||
# for debugging. Uncomment this block to write the output to
|
||||
# tmp/capybara/markdown_spec.html.
|
||||
#
|
||||
# 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
|
||||
body = get_section('no-intra-emphasis')
|
||||
expect(body.to_html).not_to match('foo<em>bar</em>baz')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Tables' do
|
||||
it 'parses table Markdown' do
|
||||
body = get_section('tables')
|
||||
expect(body).to have_selector('th:contains("Header")')
|
||||
expect(body).to have_selector('th:contains("Row")')
|
||||
expect(body).to have_selector('th:contains("Example")')
|
||||
end
|
||||
|
||||
it 'allows Markdown in tables' do
|
||||
expect(@doc.at_css('td:contains("Baz")').children.to_html).
|
||||
to eq '<strong>Baz</strong>'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Fenced Code Blocks' do
|
||||
it 'parses fenced code blocks' do
|
||||
expect(@doc).to have_selector('pre.code.highlight.white.c')
|
||||
expect(@doc).to have_selector('pre.code.highlight.white.python')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Strikethrough' do
|
||||
it 'parses strikethroughs' do
|
||||
expect(@doc).to have_selector(%{del:contains("and this text doesn't")})
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Superscript' do
|
||||
it 'parses superscript' do
|
||||
body = get_section('superscript')
|
||||
expect(body.to_html).to match('1<sup>st</sup>')
|
||||
expect(body.to_html).to match('2<sup>nd</sup>')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'HTML::Pipeline' do
|
||||
describe 'SanitizationFilter' do
|
||||
it 'uses a permissive whitelist' do
|
||||
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
|
||||
|
||||
it 'permits span elements' do
|
||||
expect(@doc).to have_selector('span:contains("span tag")')
|
||||
end
|
||||
|
||||
it 'permits table alignment' do
|
||||
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'
|
||||
|
||||
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
|
||||
|
||||
it 'removes `rel` attribute from links' do
|
||||
body = get_section('sanitizationfilter')
|
||||
expect(body).not_to have_selector('a[rel="bookmark"]')
|
||||
end
|
||||
|
||||
it "removes `href` from `a` elements if it's fishy" do
|
||||
expect(@doc).not_to have_selector('a[href*="javascript"]')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Escaping' do
|
||||
let(:table) { @doc.css('table').last.at_css('tbody') }
|
||||
|
||||
it 'escapes non-tag angle brackets' do
|
||||
expect(table.at_xpath('.//tr[1]/td[3]').inner_html).to eq '1 < 3 & 5'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Edge Cases' do
|
||||
it 'allows markup inside link elements' do
|
||||
expect(@doc.at_css('a[href="#link-emphasis"]').to_html).
|
||||
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).
|
||||
to eq %{<a href="#link-code"><code>text</code></a>}
|
||||
end
|
||||
end
|
||||
|
||||
describe 'EmojiFilter' do
|
||||
it 'parses Emoji' do
|
||||
expect(@doc).to have_selector('img.emoji', count: 10)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'TableOfContentsFilter' do
|
||||
it 'creates anchors inside header elements' do
|
||||
expect(@doc).to have_selector('h1 a#gitlab-markdown')
|
||||
expect(@doc).to have_selector('h2 a#markdown')
|
||||
expect(@doc).to have_selector('h3 a#autolinkfilter')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'AutolinkFilter' do
|
||||
let(:list) { get_section('autolinkfilter').next_element }
|
||||
|
||||
def item(index)
|
||||
list.at_css("li:nth-child(#{index})")
|
||||
end
|
||||
|
||||
it 'autolinks http://' do
|
||||
expect(item(1).children.first.name).to eq 'a'
|
||||
expect(item(1).children.first['href']).to eq 'http://about.gitlab.com/'
|
||||
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
|
||||
|
||||
it 'autolinks ftp://' do
|
||||
expect(item(3).children.first.name).to eq 'a'
|
||||
expect(item(3).children.first['href']).to eq 'ftp://ftp.us.debian.org/debian/'
|
||||
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
|
||||
|
||||
it 'autolinks irc://' do
|
||||
expect(item(5).children.first.name).to eq 'a'
|
||||
expect(item(5).children.first['href']).to eq 'irc://irc.freenode.net/git'
|
||||
end
|
||||
|
||||
it 'autolinks short, invalid URLs' do
|
||||
expect(item(6).children.first.name).to eq 'a'
|
||||
expect(item(6).children.first['href']).to eq 'http://localhost:3000'
|
||||
end
|
||||
|
||||
%w(code a kbd).each do |elem|
|
||||
it "ignores links inside '#{elem}' element" do
|
||||
body = get_section('autolinkfilter')
|
||||
expect(body).not_to have_selector("#{elem} a")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'ExternalLinkFilter' do
|
||||
let(:links) { get_section('externallinkfilter').next_element }
|
||||
|
||||
it 'adds nofollow to external link' do
|
||||
expect(links.css('a').first.to_html).to match 'nofollow'
|
||||
end
|
||||
|
||||
it 'ignores internal link' do
|
||||
expect(links.css('a').last.to_html).not_to match 'nofollow'
|
||||
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
|
||||
|
||||
it "handles references in Markdown" do
|
||||
body = get_section('reference-filters-eg-1')
|
||||
expect(body).to have_selector('em a.gfm-merge_request', count: 1)
|
||||
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
|
||||
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
|
||||
|
|
|
@ -100,6 +100,13 @@ Markdown should be usable inside a link. Let's try!
|
|||
- [**text**](#link-strong)
|
||||
- [`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
|
||||
|
||||
Because life would be :zzz: without Emoji, right? :rocket:
|
||||
|
@ -123,9 +130,9 @@ These are all plain text that should get turned into links:
|
|||
|
||||
But it shouldn't autolink text inside certain tags:
|
||||
|
||||
- <code>http://about.gitlab.com/</code>
|
||||
- <a>http://about.gitlab.com/</a>
|
||||
- <kbd>http://about.gitlab.com/</kbd>
|
||||
- <code>http://code.gitlab.com/</code>
|
||||
- <a>http://a.gitlab.com/</a>
|
||||
- <kbd>http://kbd.gitlab.com/</kbd>
|
||||
|
||||
### ExternalLinkFilter
|
||||
|
||||
|
|
|
@ -6,6 +6,14 @@ describe BlobHelper do
|
|||
let(:no_context_content) { ":type \"assem\"))" }
|
||||
let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" }
|
||||
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
|
||||
result = highlight(blob_name, no_context_content, nowrap: true, continue: false)
|
||||
|
@ -29,5 +37,15 @@ describe BlobHelper do
|
|||
result = split_content.map{ |content| highlight(blob_name, content, nowrap: true, continue: true) }
|
||||
expect(result).to eq(expected)
|
||||
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
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# encoding: UTF-8
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
module Gitlab::Markdown
|
||||
|
@ -101,6 +103,20 @@ module Gitlab::Markdown
|
|||
expect(doc.at_css('a')['href']).to eq 'http://example.com'
|
||||
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
|
||||
let(:requested_path) { 'doc/api/README.md' }
|
||||
include_examples :relative_to_requested
|
||||
|
|
|
@ -14,11 +14,14 @@
|
|||
# default_branch_protection :integer default(2)
|
||||
# twitter_sharing_enabled :boolean default(TRUE)
|
||||
# restricted_visibility_levels :text
|
||||
# version_check_enabled :boolean default(TRUE)
|
||||
# max_attachment_size :integer default(10), not null
|
||||
# session_expire_delay :integer default(10080), not null
|
||||
# default_project_visibility :integer
|
||||
# default_snippet_visibility :integer
|
||||
# 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'
|
||||
|
|
|
@ -21,12 +21,13 @@
|
|||
# import_url :string(255)
|
||||
# visibility_level :integer default(0), not null
|
||||
# archived :boolean default(FALSE), not null
|
||||
# avatar :string(255)
|
||||
# import_status :string(255)
|
||||
# repository_size :float default(0.0)
|
||||
# star_count :integer default(0), not null
|
||||
# import_type :string(255)
|
||||
# import_source :string(255)
|
||||
# avatar :string(255)
|
||||
# commit_count :integer default(0)
|
||||
#
|
||||
|
||||
require 'spec_helper'
|
||||
|
@ -111,14 +112,20 @@ describe Project do
|
|||
expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
|
||||
end
|
||||
|
||||
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}/somewhere")
|
||||
describe "#web_url" do
|
||||
let(:project) { create(:empty_project, path: "somewhere") }
|
||||
|
||||
it 'returns the full web URL for this repo' do
|
||||
expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.path}/somewhere")
|
||||
end
|
||||
end
|
||||
|
||||
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]}/somewhere")
|
||||
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
|
||||
expect(project.web_url_without_protocol).to eq("#{Gitlab.config.gitlab.url.split('://')[1]}/#{project.namespace.path}/somewhere")
|
||||
end
|
||||
end
|
||||
|
||||
describe 'last_activity methods' do
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
# otp_backup_codes :text
|
||||
# public_email :string(255) default(""), not null
|
||||
# dashboard :integer default(0)
|
||||
# project_view :integer default(0)
|
||||
#
|
||||
|
||||
require 'spec_helper'
|
||||
|
|
|
@ -6,6 +6,7 @@ describe API::API, api: true do
|
|||
let(:user) { create(:user) }
|
||||
let(:admin) { create(:admin) }
|
||||
let(:key) { create(:key, user: user) }
|
||||
let(:email) { create(:email, user: user) }
|
||||
|
||||
describe "GET /users" do
|
||||
context "when unauthenticated" do
|
||||
|
@ -384,6 +385,87 @@ describe API::API, api: true do
|
|||
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
|
||||
before { admin }
|
||||
|
||||
|
@ -528,6 +610,95 @@ describe API::API, api: true do
|
|||
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
|
||||
before { admin }
|
||||
it 'should block existing user' do
|
||||
|
|
|
@ -29,7 +29,7 @@ describe Projects::ForkService do
|
|||
it "fails due to transaction failure" do
|
||||
@to_project = fork_project(@from_project, @to_user, false)
|
||||
expect(@to_project.errors).not_to be_empty
|
||||
expect(@to_project.errors[:base]).to include("Failed to fork repository")
|
||||
expect(@to_project.errors[:base]).to include("Failed to fork repository via gitlab-shell")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
# 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
|
|
@ -0,0 +1,156 @@
|
|||
# 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
|
Loading…
Reference in New Issue