Commit 78da7b1c authored by Robert Schilling's avatar Robert Schilling

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

parents 5f3eef6e 747232ee
...@@ -2,8 +2,10 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -2,8 +2,10 @@ Please view this file on the master branch, on stable branches it's out of date.
v 7.11.0 (unreleased) v 7.11.0 (unreleased)
- Make Reply-To config apply to change e-mail confirmation and other Devise notifications (Stan Hu) - Make Reply-To config apply to change e-mail confirmation and other Devise notifications (Stan Hu)
- Add application setting to restrict user signups to e-mail domains (Stan Hu)
- Don't allow a merge request to be merged when its title starts with "WIP". - Don't allow a merge request to be merged when its title starts with "WIP".
- Add a page title to every page. - Add a page title to every page.
- Allow primary email to be set to an email that you've already added.
- Get Gitorious importer to work again. - Get Gitorious importer to work again.
- Fix clone URL field and X11 Primary selection (Dmitry Medvinsky) - Fix clone URL field and X11 Primary selection (Dmitry Medvinsky)
- Ignore invalid lines in .gitmodules - Ignore invalid lines in .gitmodules
...@@ -23,6 +25,8 @@ v 7.11.0 (unreleased) ...@@ -23,6 +25,8 @@ v 7.11.0 (unreleased)
- Don't crash when an MR from a fork has a cross-reference comment from the target project on of its commits. - Don't crash when an MR from a fork has a cross-reference comment from the target project on of its commits.
- Include commit comments in MR from a forked project. - Include commit comments in MR from a forked project.
- Fix adding new group members from admin area - Fix adding new group members from admin area
- Group milestones by title in the dashboard and all other issue views.
- Query issues, merge requests and milestones with their IID through API (Julien Bianchi)
- Add default project and snippet visibility settings to the admin web UI. - Add default project and snippet visibility settings to the admin web UI.
- Show incompatible projects in Google Code import status (Stan Hu) - Show incompatible projects in Google Code import status (Stan Hu)
- Fix bug where commit data would not appear in some subdirectories (Stan Hu) - Fix bug where commit data would not appear in some subdirectories (Stan Hu)
...@@ -30,6 +34,7 @@ v 7.11.0 (unreleased) ...@@ -30,6 +34,7 @@ v 7.11.0 (unreleased)
- Move snippets UI to fluid layout - Move snippets UI to fluid layout
- Improve UI for sidebar. Increase separation between navigation and content - Improve UI for sidebar. Increase separation between navigation and content
- Improve new project command options (Ben Bodenmiller) - Improve new project command options (Ben Bodenmiller)
- Add common method to force UTF-8 and use it to properly handle non-ascii OAuth user properties (Onur Küçük)
- Prevent sending empty messages to HipChat (Chulki Lee) - Prevent sending empty messages to HipChat (Chulki Lee)
- Improve UI for mobile phones on dashboard and project pages - Improve UI for mobile phones on dashboard and project pages
- Add room notification and message color option for HipChat - Add room notification and message color option for HipChat
......
...@@ -41,7 +41,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -41,7 +41,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:max_attachment_size, :max_attachment_size,
:default_project_visibility, :default_project_visibility,
:default_snippet_visibility, :default_snippet_visibility,
restricted_visibility_levels: [] :restricted_signup_domains_raw,
restricted_visibility_levels: [],
) )
end end
end end
...@@ -102,8 +102,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -102,8 +102,7 @@ class Admin::UsersController < Admin::ApplicationController
email = user.emails.find(params[:email_id]) email = user.emails.find(params[:email_id])
email.destroy email.destroy
user.set_notification_email user.update_secondary_emails!
user.save if user.notification_email_changed?
respond_to do |format| respond_to do |format|
format.html { redirect_to :back, notice: "Successfully removed email." } format.html { redirect_to :back, notice: "Successfully removed email." }
......
...@@ -287,40 +287,15 @@ class ApplicationController < ActionController::Base ...@@ -287,40 +287,15 @@ class ApplicationController < ActionController::Base
@filter_params @filter_params
end end
def set_filter_values(collection)
assignee_id = @filter_params[:assignee_id]
author_id = @filter_params[:author_id]
milestone_id = @filter_params[:milestone_id]
@sort = @filter_params[:sort]
@assignees = User.where(id: collection.pluck(:assignee_id))
@authors = User.where(id: collection.pluck(:author_id))
@milestones = Milestone.where(id: collection.pluck(:milestone_id))
if assignee_id.present? && !assignee_id.to_i.zero?
@assignee = @assignees.find_by(id: assignee_id)
end
if author_id.present? && !author_id.to_i.zero?
@author = @authors.find_by(id: author_id)
end
if milestone_id.present? && !milestone_id.to_i.zero?
@milestone = @milestones.find_by(id: milestone_id)
end
end
def get_issues_collection def get_issues_collection
set_filters_params set_filters_params
issues = IssuesFinder.new.execute(current_user, @filter_params) issues = IssuesFinder.new.execute(current_user, @filter_params)
set_filter_values(issues)
issues issues
end end
def get_merge_requests_collection def get_merge_requests_collection
set_filters_params set_filters_params
merge_requests = MergeRequestsFinder.new.execute(current_user, @filter_params) merge_requests = MergeRequestsFinder.new.execute(current_user, @filter_params)
set_filter_values(merge_requests)
merge_requests merge_requests
end end
......
class Profiles::EmailsController < Profiles::ApplicationController class Profiles::EmailsController < Profiles::ApplicationController
def index def index
@primary = current_user.email @primary = current_user.email
@public_email = current_user.public_email
@emails = current_user.emails @emails = current_user.emails
end end
def create def create
@email = current_user.emails.new(email_params) @email = current_user.emails.new(email_params)
flash[:alert] = @email.errors.full_messages.first unless @email.save if @email.save
NotificationService.new.new_email(@email)
else
flash[:alert] = @email.errors.full_messages.first
end
redirect_to profile_emails_url redirect_to profile_emails_url
end end
...@@ -17,9 +20,7 @@ class Profiles::EmailsController < Profiles::ApplicationController ...@@ -17,9 +20,7 @@ class Profiles::EmailsController < Profiles::ApplicationController
@email = current_user.emails.find(params[:id]) @email = current_user.emails.find(params[:id])
@email.destroy @email.destroy
current_user.set_notification_email current_user.update_secondary_emails!
current_user.set_public_email
current_user.save if current_user.notification_email_changed? or current_user.public_email_changed?
respond_to do |format| respond_to do |format|
format.html { redirect_to profile_emails_url } format.html { redirect_to profile_emails_url }
......
...@@ -113,8 +113,9 @@ class IssuableFinder ...@@ -113,8 +113,9 @@ class IssuableFinder
end end
def by_milestone(items) def by_milestone(items)
if params[:milestone_id].present? if params[:milestone_title].present?
items = items.where(milestone_id: (params[:milestone_id] == NONE ? nil : params[:milestone_id])) milestone_ids = (params[:milestone_title] == NONE ? nil : Milestone.where(title: params[:milestone_title]).pluck(:id))
items = items.where(milestone_id: milestone_ids)
end end
items items
......
...@@ -28,6 +28,7 @@ module MilestonesHelper ...@@ -28,6 +28,7 @@ module MilestonesHelper
Milestone.where(project_id: @projects) Milestone.where(project_id: @projects)
end.active end.active
options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id]) grouped_milestones = Milestones::GroupService.new(milestones).execute
options_from_collection_for_select(grouped_milestones, 'title', 'title', params[:milestone_title])
end end
end end
...@@ -18,11 +18,13 @@ ...@@ -18,11 +18,13 @@
# restricted_visibility_levels :text # restricted_visibility_levels :text
# max_attachment_size :integer default(10) # max_attachment_size :integer default(10)
# default_project_visibility :integer # default_project_visibility :integer
# default_snippet_visibility :integer # restricted_signup_domains :text
# #
class ApplicationSetting < ActiveRecord::Base class ApplicationSetting < ActiveRecord::Base
serialize :restricted_visibility_levels serialize :restricted_visibility_levels
serialize :restricted_signup_domains, Array
attr_accessor :restricted_signup_domains_raw
validates :home_page_url, validates :home_page_url,
allow_blank: true, allow_blank: true,
...@@ -55,11 +57,29 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -55,11 +57,29 @@ class ApplicationSetting < ActiveRecord::Base
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
max_attachment_size: Settings.gitlab['max_attachment_size'], max_attachment_size: Settings.gitlab['max_attachment_size'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'] default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
restricted_signup_domains: Settings.gitlab['restricted_signup_domains']
) )
end end
def home_page_url_column_exist def home_page_url_column_exist
ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url) ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url)
end end
def restricted_signup_domains_raw
self.restricted_signup_domains.join("\n") unless self.restricted_signup_domains.nil?
end
def restricted_signup_domains_raw=(values)
self.restricted_signup_domains = []
self.restricted_signup_domains = values.split(
/\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
| # or
\s # any whitespace character
| # or
[\r\n] # any number of newline characters
/x)
self.restricted_signup_domains.reject! { |d| d.empty? }
end
end end
...@@ -18,7 +18,6 @@ class Email < ActiveRecord::Base ...@@ -18,7 +18,6 @@ class Email < ActiveRecord::Base
validates :email, presence: true, email: { strict_mode: true }, uniqueness: true validates :email, presence: true, email: { strict_mode: true }, uniqueness: true
validate :unique_email, if: ->(email) { email.email_changed? } validate :unique_email, if: ->(email) { email.email_changed? }
after_create :notify
before_validation :cleanup_email before_validation :cleanup_email
def cleanup_email def cleanup_email
...@@ -28,8 +27,4 @@ class Email < ActiveRecord::Base ...@@ -28,8 +27,4 @@ class Email < ActiveRecord::Base
def unique_email def unique_email
self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email) self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email)
end end
def notify
NotificationService.new.new_email(self)
end
end end
...@@ -139,13 +139,16 @@ class User < ActiveRecord::Base ...@@ -139,13 +139,16 @@ class User < ActiveRecord::Base
validate :avatar_type, if: ->(user) { user.avatar_changed? } validate :avatar_type, if: ->(user) { user.avatar_changed? }
validate :unique_email, if: ->(user) { user.email_changed? } validate :unique_email, if: ->(user) { user.email_changed? }
validate :owns_notification_email, if: ->(user) { user.notification_email_changed? } validate :owns_notification_email, if: ->(user) { user.notification_email_changed? }
validate :owns_public_email, if: ->(user) { user.public_email_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i } validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :generate_password, on: :create before_validation :generate_password, on: :create
before_validation :restricted_signup_domains, on: :create
before_validation :sanitize_attrs before_validation :sanitize_attrs
before_validation :set_notification_email, if: ->(user) { user.email_changed? } before_validation :set_notification_email, if: ->(user) { user.email_changed? }
before_validation :set_public_email, if: ->(user) { user.public_email_changed? } before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? }
before_save :ensure_authentication_token before_save :ensure_authentication_token
after_save :ensure_namespace_correct after_save :ensure_namespace_correct
after_initialize :set_projects_limit after_initialize :set_projects_limit
...@@ -276,13 +279,29 @@ class User < ActiveRecord::Base ...@@ -276,13 +279,29 @@ class User < ActiveRecord::Base
end end
def unique_email def unique_email
self.errors.add(:email, 'has already been taken') if Email.exists?(email: self.email) if !self.emails.exists?(email: self.email) && Email.exists?(email: self.email)
self.errors.add(:email, 'has already been taken')
end
end end
def owns_notification_email def owns_notification_email
self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email) self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email)
end end
def owns_public_email
self.errors.add(:public_email, "is not an email you own") unless self.all_emails.include?(self.public_email)
end
def update_emails_with_primary_email
primary_email_record = self.emails.find_by(email: self.email)
if primary_email_record
primary_email_record.destroy
self.emails.create(email: self.email_was)
self.update_secondary_emails!
end
end
# Groups user has access to # Groups user has access to
def authorized_groups def authorized_groups
@authorized_groups ||= begin @authorized_groups ||= begin
...@@ -448,10 +467,16 @@ class User < ActiveRecord::Base ...@@ -448,10 +467,16 @@ class User < ActiveRecord::Base
def set_public_email def set_public_email
if self.public_email.blank? || !self.all_emails.include?(self.public_email) if self.public_email.blank? || !self.all_emails.include?(self.public_email)
self.public_email = '' self.public_email = nil
end end
end end
def update_secondary_emails!
self.set_notification_email
self.set_public_email
self.save if self.notification_email_changed? || self.public_email_changed?
end
def set_projects_limit def set_projects_limit
connection_default_value_defined = new_record? && !projects_limit_changed? connection_default_value_defined = new_record? && !projects_limit_changed?
return unless self.projects_limit.nil? || connection_default_value_defined return unless self.projects_limit.nil? || connection_default_value_defined
...@@ -611,4 +636,27 @@ class User < ActiveRecord::Base ...@@ -611,4 +636,27 @@ class User < ActiveRecord::Base
select(:project_id). select(:project_id).
uniq.map(&:project_id) uniq.map(&:project_id)
end end
def restricted_signup_domains
email_domains = current_application_settings.restricted_signup_domains
unless email_domains.blank?
match_found = email_domains.any? do |domain|
escaped = Regexp.escape(domain).gsub('\*','.*?')
regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
email_domain = Mail::Address.new(self.email).domain
email_domain =~ regexp
end
unless match_found
self.errors.add :email,
'is not whitelisted. ' +
'Email domains valid for registration are: ' +
email_domains.join(', ')
return false
end
end
true
end
end end
...@@ -72,6 +72,11 @@ ...@@ -72,6 +72,11 @@
= f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2' = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
= f.number_field :max_attachment_size, class: 'form-control' = f.number_field :max_attachment_size, class: 'form-control'
.form-group
= f.label :restricted_signup_domains, 'Restricted domains for sign-ups', class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :restricted_signup_domains_raw, placeholder: 'domain.com', class: 'form-control'
.help-block Ex: domain.com, *.domain.com. Wildcards allowed. Use separate lines for multiple entries.
.form-actions .form-actions
= f.submit 'Save', class: 'btn btn-primary' = f.submit 'Save', class: 'btn btn-primary'
...@@ -5,11 +5,15 @@ ...@@ -5,11 +5,15 @@
Your Your
%b Primary Email %b Primary Email
will be used for avatar detection and web based operations, such as edits and merges. will be used for avatar detection and web based operations, such as edits and merges.
%br %p.light
Your Your
%b Notification Email %b Notification Email
will be used for account notifications. will be used for account notifications.
%br %p.light
Your
%b Public Email
will be displayed on your public profile.
%p.light
All email addresses will be used to identify your commits. All email addresses will be used to identify your commits.
%hr %hr
...@@ -21,13 +25,17 @@ ...@@ -21,13 +25,17 @@
%li %li
%strong= @primary %strong= @primary
%span.label.label-success Primary Email %span.label.label-success Primary Email
- if @primary === @public_email - if @primary === current_user.public_email
%span.label.label-info Public Email %span.label.label-info Public Email
- if @primary === current_user.notification_email
%span.label.label-info Notification Email
- @emails.each do |email| - @emails.each do |email|
%li %li
%strong= email.email %strong= email.email
- if email.email === @public_email - if email.email === current_user.public_email
%span.label.label-info Public Email %span.label.label-info Public Email
- if email.email === current_user.notification_email
%span.label.label-info Notification Email
%span.cgray %span.cgray
added #{time_ago_with_tooltip(email.created_at)} added #{time_ago_with_tooltip(email.created_at)}
= link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove pull-right' = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove pull-right'
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
#{state_filters_text_for(:all, @project)} #{state_filters_text_for(:all, @project)}
.issues-details-filters .issues-details-filters
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_id, :label_name]), method: :get, class: 'filter-form' do = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name]), method: :get, class: 'filter-form' do
- if controller.controller_name == 'issues' - if controller.controller_name == 'issues'
.check-all-holder .check-all-holder
= check_box_tag "check_all_issues", nil, false, = check_box_tag "check_all_issues", nil, false,
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
placeholder: 'Author', class: 'trigger-submit', any_user: true, first_user: true) placeholder: 'Author', class: 'trigger-submit', any_user: true, first_user: true)
.filter-item.inline.milestone-filter .filter-item.inline.milestone-filter
= select_tag('milestone_id', projects_milestones_options, class: "select2 trigger-submit", prompt: 'Milestone') = select_tag('milestone_title', projects_milestones_options, class: "select2 trigger-submit", prompt: 'Milestone')
- if @project - if @project
.filter-item.inline.labels-filter .filter-item.inline.labels-filter
......
...@@ -132,6 +132,7 @@ Settings.gitlab.default_projects_features['wiki'] = true if Settings.g ...@@ -132,6 +132,7 @@ Settings.gitlab.default_projects_features['wiki'] = true if Settings.g
Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil?
Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
Settings.gitlab['repository_downloads_path'] = File.absolute_path(Settings.gitlab['repository_downloads_path'] || 'tmp/repositories', Rails.root) Settings.gitlab['repository_downloads_path'] = File.absolute_path(Settings.gitlab['repository_downloads_path'] || 'tmp/repositories', Rails.root)
Settings.gitlab['restricted_signup_domains'] ||= []
# #
# Gravatar # Gravatar
......
class AddRestrictedSignupDomainsToApplicationSettings < ActiveRecord::Migration
def change
add_column :application_settings, :restricted_signup_domains, :text
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20150429002313) do ActiveRecord::Schema.define(version: 20150502064022) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -31,6 +31,7 @@ ActiveRecord::Schema.define(version: 20150429002313) do ...@@ -31,6 +31,7 @@ ActiveRecord::Schema.define(version: 20150429002313) do
t.integer "max_attachment_size", default: 10, null: false t.integer "max_attachment_size", default: 10, null: false
t.integer "default_project_visibility" t.integer "default_project_visibility"
t.integer "default_snippet_visibility" t.integer "default_snippet_visibility"
t.text "restricted_signup_domains"
end end
create_table "broadcast_messages", force: true do |t| create_table "broadcast_messages", force: true do |t|
......
...@@ -35,7 +35,7 @@ Parameters: ...@@ -35,7 +35,7 @@ Parameters:
## New group ## New group
Creates a new project group. Available only for admin. Creates a new project group. Available only for users who can create groups.
``` ```
POST /groups POST /groups
......
...@@ -99,11 +99,13 @@ GET /projects/:id/issues?labels=foo,bar ...@@ -99,11 +99,13 @@ GET /projects/:id/issues?labels=foo,bar
GET /projects/:id/issues?labels=foo,bar&state=opened GET /projects/:id/issues?labels=foo,bar&state=opened
GET /projects/:id/issues?milestone=1.0.0 GET /projects/:id/issues?milestone=1.0.0
GET /projects/:id/issues?milestone=1.0.0&state=opened GET /projects/:id/issues?milestone=1.0.0&state=opened
GET /projects/:id/issues?iid=42
``` ```
Parameters: Parameters:
- `id` (required) - The ID of a project - `id` (required) - The ID of a project
- `iid` (optional) - Return the issue having the given `iid`
- `state` (optional) - Return `all` issues or just those that are `opened` or `closed` - `state` (optional) - Return `all` issues or just those that are `opened` or `closed`
- `labels` (optional) - Comma-separated list of label names - `labels` (optional) - Comma-separated list of label names
- `milestone` (optional) - Milestone title - `milestone` (optional) - Milestone title
......
...@@ -10,11 +10,13 @@ The pagination parameters `page` and `per_page` can be used to restrict the list ...@@ -10,11 +10,13 @@ The pagination parameters `page` and `per_page` can be used to restrict the list
GET /projects/:id/merge_requests GET /projects/:id/merge_requests
GET /projects/:id/merge_requests?state=opened GET /projects/:id/merge_requests?state=opened
GET /projects/:id/merge_requests?state=all GET /projects/:id/merge_requests?state=all
GET /projects/:id/merge_requests?iid=42
``` ```
Parameters: Parameters:
- `id` (required) - The ID of a project - `id` (required) - The ID of a project
- `iid` (optional) - Return the request having the given `iid`
- `state` (optional) - Return `all` requests or just those that are `merged`, `opened` or `closed` - `state` (optional) - Return `all` requests or just those that are `merged`, `opened` or `closed`
- `order_by` (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` - `order_by` (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` - `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
...@@ -388,6 +390,6 @@ Parameters: ...@@ -388,6 +390,6 @@ Parameters:
] ]
``` ```
## Comments on issues ## Comments on merge requets
Comments are done via the notes resource. Comments are done via the notes resource.
...@@ -6,6 +6,7 @@ Returns a list of project milestones. ...@@ -6,6 +6,7 @@ Returns a list of project milestones.
``` ```
GET /projects/:id/milestones GET /projects/:id/milestones
GET /projects/:id/milestones?iid=42
``` ```
```json ```json
...@@ -27,6 +28,7 @@ GET /projects/:id/milestones ...@@ -27,6 +28,7 @@ GET /projects/:id/milestones
Parameters: Parameters:
- `id` (required) - The ID of a project - `id` (required) - The ID of a project
- `iid` (optional) - Return the milestone having the given `iid`
## Get single milestone ## Get single milestone
......
...@@ -20,7 +20,7 @@ module API ...@@ -20,7 +20,7 @@ module API
present @groups, with: Entities::Group present @groups, with: Entities::Group
end end
# Create group. Available only for admin # Create group. Available only for users who can create groups.
# #
# Parameters: # Parameters:
# name (required) - The name of the group # name (required) - The name of the group
...@@ -28,7 +28,7 @@ module API ...@@ -28,7 +28,7 @@ module API
# Example Request: # Example Request:
# POST /groups # POST /groups
post do post do
authenticated_as_admin! authorize! :create_group, current_user
required_attributes! [:name, :path] required_attributes! [:name, :path]
attrs = attributes_for_keys [:name, :path, :description] attrs = attributes_for_keys [:name, :path, :description]
......
...@@ -173,6 +173,10 @@ module API ...@@ -173,6 +173,10 @@ module API
end end
end end
def filter_by_iid(items, iid)
items.where(iid: iid)
end
# error helpers # error helpers
def forbidden!(reason = nil) def forbidden!(reason = nil)
......
...@@ -51,6 +51,7 @@ module API ...@@ -51,6 +51,7 @@ module API
# #
# Parameters: # Parameters:
# id (required) - The ID of a project # id (required) - The ID of a project
# iid (optional) - Return the project issue having the given `iid`
# state (optional) - Return "opened" or "closed" issues # state (optional) - Return "opened" or "closed" issues
# labels (optional) - Comma-separated list of label names # labels (optional) - Comma-separated list of label names
# milestone (optional) - Milestone title # milestone (optional) - Milestone title
...@@ -66,10 +67,12 @@ module API ...@@ -66,10 +67,12 @@ module API
# GET /projects/:id/issues?labels=foo,bar&state=opened # GET /projects/:id/issues?labels=foo,bar&state=opened
# GET /projects/:id/issues?milestone=1.0.0 # GET /projects/:id/issues?milestone=1.0.0
# GET /projects/:id/issues?milestone=1.0.0&state=closed # GET /projects/:id/issues?milestone=1.0.0&state=closed
# GET /issues?iid=42
get ":id/issues" do get ":id/issues" do
issues = user_project.issues issues = user_project.issues
issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil?
unless params[:milestone].nil? unless params[:milestone].nil?
issues = filter_issues_milestone(issues, params[:milestone]) issues = filter_issues_milestone(issues, params[:milestone])
......
...@@ -24,6 +24,7 @@ module API ...@@ -24,6 +24,7 @@ module API
# #
# Parameters: # Parameters:
# id (required) - The ID of a project # id (required) - The ID of a project
# iid (optional) - Return the project MR having the given `iid`
# state (optional) - Return requests "merged", "opened" or "closed" # state (optional) - Return requests "merged", "opened" or "closed"
# order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
# sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
...@@ -36,11 +37,16 @@ module API ...@@ -36,11 +37,16 @@ module API
# GET /projects/:id/merge_requests?order_by=updated_at # GET /projects/:id/merge_requests?order_by=updated_at
# GET /projects/:id/merge_requests?sort=desc # GET /projects/:id/merge_requests?sort=desc
# GET /projects/:id/merge_requests?sort=asc # GET /projects/:id/merge_requests?sort=asc
# GET /projects/:id/merge_requests?iid=42
# #
get ":id/merge_requests" do get ":id/merge_requests" do
authorize! :read_merge_request, user_project authorize! :read_merge_request, user_project
merge_requests = user_project.merge_requests merge_requests = user_project.merge_requests
unless params[:iid].nil?
merge_requests = filter_by_iid(merge_requests, params[:iid])
end
merge_requests = merge_requests =
case params["state"] case params["state"]
when "opened" then merge_requests.opened when "opened" then merge_requests.opened
...@@ -169,8 +175,8 @@ module API ...@@ -169,8 +175,8 @@ module API
# Merge MR # Merge MR
# #
# Parameters: # Parameters:
# id (required) - The ID of a project # id (required) - The ID of a project
# merge_request_id (required) - ID of MR # merge_request_id (required) - ID of MR
# merge_commit_message (optional) - Custom merge commit message # merge_commit_message (optional) - Custom merge commit message
# Example: # Example:
# PUT /projects/:id/merge_request/:merge_request_id/merge # PUT /projects/:id/merge_request/:merge_request_id/merge
...@@ -209,7 +215,7 @@ module API ...@@ -209,7 +215,7 @@ module API
# Get a merge request's comments # Get a merge request's comments
# #
# Parameters: # Parameters:
# id (required) - The ID of a project # id (required) - The ID of a project
# merge_request_id (required) - ID of MR # merge_request_id (required) - ID of MR
# Examples: # Examples:
# GET /projects/:id/merge_request/:merge_request_id/comments # GET /projects/:id/merge_request/:merge_request_id/comments
...@@ -225,9 +231,9 @@ module API ...@@ -225,9 +231,9 @@ module API
# Post comment to merge request # Post comment to merge request
# #
# Parameters: # Parameters:
# id (required) - The ID of a project # id (required) - The ID of a project
# merge_request_id (required) - ID of MR # merge_request_id (required) - ID of MR
# note (required) - Text of comment # note (required) - Text of comment
# Examples: # Examples:
# POST /projects/:id/merge_request/:merge_request_id/comments # POST /projects/:id/merge_request/:merge_request_id/comments
# #
......
...@@ -9,11 +9,11 @@ module Gitlab ...@@ -9,11 +9,11 @@ module Gitlab
end end
def uid def uid
auth_hash.uid.to_s Gitlab::Utils.force_utf8(auth_hash.uid.to_s)
end end
def provider def provider
auth_hash.provider Gitlab::Utils.force_utf8(auth_hash.provider.to_s)
end end
def info def info
...@@ -21,23 +21,28 @@ module Gitlab ...@@ -21,23 +21,28 @@ module Gitlab
end end
def name def name
(info.try(:name) || full_name).to_s.force_encoding('utf-8') Gitlab::Utils.force_utf8((info.try(:name) || full_name).to_s)
end end
def full_name def full_name
"#{info.first_name} #{info.last_name}" Gitlab::Utils.force_utf8("#{info.first_name} #{info.last_name}")
end end
def username def username
(info.try(:nickname) || generate_username).to_s.force_encoding('utf-8') Gitlab::Utils.force_utf8(
(info.try(:nickname) || generate_username).to_s
)
end end
def email def email
(info.try(:email) || generate_temporarily_email).downcase Gitlab::Utils.force_utf8(
(info.try(:email) || generate_temporarily_email).downcase
)
end end
def password def password
@password ||= Devise.friendly_token[0, 8].downcase devise_friendly_token = Devise.friendly_token[0, 8].downcase
@password ||= Gitlab::Utils.force_utf8(devise_friendly_token)
end end
# Get the first part of the email address (before @) # Get the first part of the email address (before @)
......
...@@ -9,5 +9,9 @@ module Gitlab ...@@ -9,5 +9,9 @@ module Gitlab
def system_silent(cmd) def system_silent(cmd)
Popen::popen(cmd).last.zero? Popen::popen(cmd).last.zero?
end end
def force_utf8(str)
str.force_encoding(Encoding::UTF_8)
end
end end
end end
...@@ -22,6 +22,7 @@ FactoryGirl.define do ...@@ -22,6 +22,7 @@ FactoryGirl.define do
password "12345678" password "12345678"
confirmed_at { Time.now } confirmed_at { Time.now }
confirmation_token { nil } confirmation_token { nil }
can_create_group true
trait :admin do trait :admin do
admin true admin true
......
...@@ -95,7 +95,7 @@ describe 'Issues', feature: true do ...@@ -95,7 +95,7 @@ describe 'Issues', feature: true do
let(:issue) { @issue } let(:issue) { @issue }
it 'should allow filtering by issues with no specified milestone' do it 'should allow filtering by issues with no specified milestone' do
visit namespace_project_issues_path(project.namespace, project, milestone_id: IssuableFinder::NONE) visit namespace_project_issues_path(project.namespace, project, milestone_title: IssuableFinder::NONE)
expect(page).not_to have_content 'foobar' expect(page).not_to have_content 'foobar'
expect(page).to have_content 'barbaz' expect(page).to have_content 'barbaz'
...@@ -103,7 +103,7 @@ describe 'Issues', feature: true do ...@@ -103,7 +103,7 @@ describe 'Issues', feature: true do
end end
it 'should allow filtering by a specified milestone' do it 'should allow filtering by a specified milestone' do
visit namespace_project_issues_path(project.namespace, project, milestone_id: issue.milestone.id) visit namespace_project_issues_path(project.namespace, project, milestone_title: issue.milestone.title)
expect(page).to have_content 'foobar' expect(page).to have_content 'foobar'
expect(page).not_to have_content 'barbaz' expect(page).not_to have_content 'barbaz'
......
...@@ -43,7 +43,7 @@ describe IssuesFinder do ...@@ -43,7 +43,7 @@ describe IssuesFinder do
end end
it 'should filter by milestone id' do it 'should filter by milestone id' do
params = { scope: "all", milestone_id: milestone.id, state: 'opened' } params = { scope: "all", milestone_title: milestone.title, state: 'opened' }
issues = IssuesFinder.new.execute(user, params) issues = IssuesFinder.new.execute(user, params)
expect(issues).to eq([issue1]) expect(issues).to eq([issue1])
end end
......
...@@ -2,54 +2,109 @@ require 'spec_helper' ...@@ -2,54 +2,109 @@ require 'spec_helper'
describe Gitlab::OAuth::AuthHash do describe Gitlab::OAuth::AuthHash do
let(:auth_hash) do let(:auth_hash) do
Gitlab::OAuth::AuthHash.new(double({ Gitlab::OAuth::AuthHash.new(
provider: 'twitter', double({
uid: uid, provider: provider_ascii,
info: double(info_hash) uid: uid_ascii,
})) info: double(info_hash)
})
)
end end
let(:uid) { 'my-uid' }
let(:email) { 'my-email@example.com' } let(:uid_raw) {
let(:nickname) { 'my-nickname' } "CN=Onur K\xC3\xBC\xC3\xA7\xC3\xBCk,OU=Test,DC=example,DC=net"
}
let(:email_raw) { "onur.k\xC3\xBC\xC3\xA7\xC3\xBCk@example.net" }
let(:nickname_raw) { "ok\xC3\xBC\xC3\xA7\xC3\xBCk" }
let(:first_name_raw) { 'Onur' }
let(:last_name_raw) { "K\xC3\xBC\xC3\xA7\xC3\xBCk" }
let(:name_raw) { "Onur K\xC3\xBC\xC3\xA7\xC3\xBCk" }
let(:provider_ascii) { 'ldap'.force_encoding(Encoding::ASCII_8BIT) }
let(:uid_ascii) { uid_raw.force_encoding(Encoding::ASCII_8BIT) }
let(:email_ascii) { email_raw.force_encoding(Encoding::ASCII_8BIT) }
let(:nickname_ascii) { nickname_raw.force_encoding(Encoding::ASCII_8BIT) }
let(:first_name_ascii) { first_name_raw.force_encoding(Encoding::ASCII_8BIT) }
let(:last_name_ascii) { last_name_raw.force_encoding(Encoding::ASCII_8BIT) }
let(:name_ascii) { name_raw.force_encoding(Encoding::ASCII_8BIT) }
let(:provider_utf8) { provider_ascii.force_encoding(Encoding::UTF_8) }
let(:uid_utf8) { uid_ascii.force_encoding(Encoding::UTF_8) }
let(:email_utf8) { email_ascii.force_encoding(Encoding::UTF_8) }
let(:nickname_utf8) { nickname_ascii.force_encoding(Encoding::UTF_8) }
let(:name_utf8) { name_ascii.force_encoding(Encoding::UTF_8) }
let(:info_hash) { let(:info_hash) {
{ {
email: email, email: email_ascii,
nickname: nickname, first_name: first_name_ascii,
name: 'John', last_name: last_name_ascii,
first_name: "John", name: name_ascii,
last_name: "Who" nickname: nickname_ascii,
uid: uid_ascii
} }
} }
context "defaults" do context 'defaults' do
it { expect(auth_hash.provider).to eql 'twitter' } it { expect(auth_hash.provider).to eql provider_utf8 }
it { expect(auth_hash.uid).to eql uid } it { expect(auth_hash.uid).to eql uid_utf8 }
it { expect(auth_hash.email).to eql email } it { expect(auth_hash.email).to eql email_utf8 }
it { expect(auth_hash.username).to eql nickname } it { expect(auth_hash.username).to eql nickname_utf8 }
it { expect(auth_hash.name).to eql "John" } it { expect(auth_hash.name).to eql name_utf8 }
it { expect(auth_hash.password).to_not be_empty } it { expect(auth_hash.password).to_not be_empty }
end end
context "email not provided" do context 'email not provided' do
before { info_hash.delete(:email) } before { info_hash.delete(:email) }
it "generates a temp email" do
it 'generates a temp email' do
expect( auth_hash.email).to start_with('temp-email-for-oauth') expect( auth_hash.email).to start_with('temp-email-for-oauth')
end end
end end
context "username not provided" do context 'username not provided' do
before { info_hash.delete(:nickname) } before { info_hash.delete(:nickname) }
it "takes the first part of the email as username" do it 'takes the first part of the email as username' do
expect( auth_hash.username ).to eql "my-email" expect(auth_hash.username).to eql 'onur-kucuk'
end end
end end
context "name not provided" do context 'name not provided' do
before { info_hash.delete(:name) } before { info_hash.delete(:name) }
it "concats first and lastname as the name" do it 'concats first and lastname as the name' do
expect( auth_hash.name ).to eql "John Who" expect(auth_hash.name).to eql name_utf8
end
end
context 'auth_hash constructed with ASCII-8BIT encoding' do
it 'forces utf8 encoding on uid' do
auth_hash.uid.encoding.should eql Encoding::UTF_8
end
it 'forces utf8 encoding on provider' do
auth_hash.provider.encoding.should eql Encoding::UTF_8
end
it 'forces utf8 encoding on name' do
auth_hash.name.encoding.should eql Encoding::UTF_8
end
it 'forces utf8 encoding on full_name' do
auth_hash.full_name.encoding.should eql Encoding::UTF_8
end
it 'forces utf8 encoding on username' do
auth_hash.username.encoding.should eql Encoding::UTF_8
end
it 'forces utf8 encoding on email' do
auth_hash.email.encoding.should eql Encoding::UTF_8
end
it 'forces utf8 encoding on password' do
auth_hash.password.encoding.should eql Encoding::UTF_8
end end
end end
end end
\ No newline at end of file
...@@ -21,4 +21,28 @@ require 'spec_helper' ...@@ -21,4 +21,28 @@ require 'spec_helper'
describe ApplicationSetting, models: true do describe ApplicationSetting, models: true do
it { expect(ApplicationSetting.create_from_defaults).to be_valid } it { expect(ApplicationSetting.create_from_defaults).to be_valid }
context 'restricted signup domains' do
let(:setting) { ApplicationSetting.create_from_defaults }
it 'set single domain' do
setting.restricted_signup_domains_raw = 'example.com'
expect(setting.restricted_signup_domains).to eq(['example.com'])
end
it 'set multiple domains with spaces' do
setting.restricted_signup_domains_raw = 'example.com *.example.com'
expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com'])
end
it 'set multiple domains with newlines and a space' do
setting.restricted_signup_domains_raw = "example.com\n *.example.com"
expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com'])
end
it 'set multiple domains with commas' do
setting.restricted_signup_domains_raw = "example.com, *.example.com"
expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com'])
end
end
end end
...@@ -54,6 +54,8 @@ ...@@ -54,6 +54,8 @@
require 'spec_helper' require 'spec_helper'
describe User do describe User do
include Gitlab::CurrentSettings
describe "Associations" do describe "Associations" do
it { is_expected.to have_one(:namespace) } it { is_expected.to have_one(:namespace) }
it { is_expected.to have_many(:snippets).class_name('Snippet').dependent(:destroy) } it { is_expected.to have_many(:snippets).class_name('Snippet').dependent(:destroy) }
...@@ -112,6 +114,51 @@ describe User do ...@@ -112,6 +114,51 @@ describe User do
user = build(:user, email: "lol!'+=?><#$%^&*()@gmail.com") user = build(:user, email: "lol!'+=?><#$%^&*()@gmail.com")
expect(user).to be_invalid expect(user).to be_invalid
end end
context 'when no signup domains listed' do
before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return([]) }
it 'accepts any email' do
user = build(:user, email: "info@example.com")
expect(user).to be_valid
end
end
context 'when a signup domain is listed and subdomains are allowed' do
before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return(['example.com', '*.example.com']) }
it 'accepts info@example.com' do
user = build(:user, email: "info@example.com")
expect(user).to be_valid
end
it 'accepts info@test.example.com' do
user = build(:user, email: "info@test.example.com")
expect(user).to be_valid
end
it 'rejects example@test.com' do
user = build(:user, email: "example@test.com")
expect(user).to be_invalid
end
end
context 'when a signup domain is listed and subdomains are not allowed' do
before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return(['example.com']) }
it 'accepts info@example.com' do
user = build(:user, email: "info@example.com")
expect(user).to be_valid
end
it 'rejects info@test.example.com' do
user = build(:user, email: "info@test.example.com")
expect(user).to be_invalid
end
it 'rejects example@test.com' do
user = build(:user, email: "example@test.com")
expect(user).to be_invalid
end
end
end end
end end
......
...@@ -3,8 +3,9 @@ require 'spec_helper' ...@@ -3,8 +3,9 @@ require 'spec_helper'
describe API::API, api: true do describe API::API, api: true do
include ApiHelpers include ApiHelpers
let(:user1) { create(:user) } let(:user1) { create(:user, can_create_group: false) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let!(:group1) { create(:group) } let!(:group1) { create(:group) }
let!(:group2) { create(:group) } let!(:group2) { create(:group) }
...@@ -94,32 +95,32 @@ describe API::API, api: true do ...@@ -94,32 +95,32 @@ describe API::API, api: true do
end end
describe "POST /groups" do describe "POST /groups" do
context "when authenticated as user" do context "when authenticated as user without group permissions" do
it "should not create group" do it "should not create group" do
post api("/groups", user1), attributes_for(:group) post api("/groups", user1), attributes_for(:group)
expect(response.status).to eq(403) expect(response.status).to eq(403)
end end
end end
context "when authenticated as admin" do context "when authenticated as user with group permissions" do
it "should create group" do it "should create group" do
post api("/groups", admin), attributes_for(:group) post api("/groups", user3), attributes_for(:group)
expect(response.status).to eq(201) expect(response.status).to eq(201)
end end
it "should not create group, duplicate" do it "should not create group, duplicate" do
post api("/groups", admin), {name: "Duplicate Test", path: group2.path} post api("/groups", user3), {name: 'Duplicate Test', path: group2.path}
expect(response.status).to eq(400) expect(response.status).to eq(400)
expect(response.message).to eq("Bad Request") expect(response.message).to eq("Bad Request")
end end
it "should return 400 bad request error if name not given" do it "should return 400 bad request error if name not given" do
post api("/groups", admin), {path: group2.path} post api("/groups", user3), {path: group2.path}
expect(response.status).to eq(400) expect(response.status).to eq(400)
end end
it "should return 400 bad request error if path not given" do it "should return 400 bad request error if path not given" do
post api("/groups", admin), { name: 'test' } post api("/groups", user3), {name: 'test'}
expect(response.status).to eq(400) expect(response.status).to eq(400)
end end
end end
...@@ -133,8 +134,8 @@ describe API::API, api: true do ...@@ -133,8 +134,8 @@ describe API::API, api: true do
end end
it "should not remove a group if not an owner" do it "should not remove a group if not an owner" do
user3 = create(:user) user4 = create(:user)
group1.add_user(user3, Gitlab::Access::MASTER) group1.add_user(user4, Gitlab::Access::MASTER)
delete api("/groups/#{group1.id}", user3) delete api("/groups/#{group1.id}", user3)
expect(response.status).to eq(403) expect(response.status).to eq(403)
end end
......
...@@ -194,6 +194,14 @@ describe API::API, api: true do ...@@ -194,6 +194,14 @@ describe API::API, api: true do
expect(json_response['iid']).to eq(issue.iid) expect(json_response['iid']).to eq(issue.iid)
end end
it 'should return a project issue by iid' do
get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
response.status.should == 200
json_response.first['title'].should == issue.title
json_response.first['id'].should == issue.id
json_response.first['iid'].should == issue.iid
end
it "should return 404 if issue id not found" do it "should return 404 if issue id not found" do
get api("/projects/#{project.id}/issues/54321", user) get api("/projects/#{project.id}/issues/54321", user)
expect(response.status).to eq(404) expect(response.status).to eq(404)
......
...@@ -115,6 +115,14 @@ describe API::API, api: true do ...@@ -115,6 +115,14 @@ describe API::API, api: true do
expect(json_response['iid']).to eq(merge_request.iid) expect(json_response['iid']).to eq(merge_request.iid)
end end
it 'should return merge_request by iid' do
url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}"
get api(url, user)
response.status.should == 200
json_response.first['title'].should == merge_request.title
json_response.first['id'].should == merge_request.id
end
it "should return a 404 error if merge_request_id not found" do it "should return a 404 error if merge_request_id not found" do
get api("/projects/#{project.id}/merge_request/999", user) get api("/projects/#{project.id}/merge_request/999", user)
expect(response.status).to eq(404) expect(response.status).to eq(404)
......
...@@ -30,6 +30,13 @@ describe API::API, api: true do ...@@ -30,6 +30,13 @@ describe API::API, api: true do
expect(json_response['iid']).to eq(milestone.iid) expect(json_response['iid']).to eq(milestone.iid)
end end
it 'should return a project milestone by iid' do
get api("/projects/#{project.id}/milestones?iid=#{milestone.iid}", user)
response.status.should == 200
json_response.first['title'].should == milestone.title
json_response.first['id'].should == milestone.id
end
it 'should return 401 error if user not authenticated' do it 'should return 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones/#{milestone.id}") get api("/projects/#{project.id}/milestones/#{milestone.id}")
expect(response.status).to eq(401) expect(response.status).to eq(401)
......
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