Commit 75486f09 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'master' into zj/gitlab-ce-merge-if-green

parents e7969d6f f5430e48
...@@ -2,19 +2,30 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -2,19 +2,30 @@ Please view this file on the master branch, on stable branches it's out of date.
v 8.3.0 (unreleased) v 8.3.0 (unreleased)
- Merge when build succeeds (Zeger-Jan van de Weg) - Merge when build succeeds (Zeger-Jan van de Weg)
- Fix API setting of 'public' attribute to false will make a project private (Stan Hu)
- Handle and report SSL errors in Web hook test (Stan Hu)
- Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera)
- Fix 500 error when update group member permission - Fix 500 error when update group member permission
- Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera)
- Recognize issue/MR/snippet/commit links as references - Recognize issue/MR/snippet/commit links as references
- Add ignore whitespace change option to commit view - Add ignore whitespace change option to commit view
- Fire update hook from GitLab - Fire update hook from GitLab
- Style warning about mentioning many people in a comment
- Fix: sort milestones by due date once again (Greg Smethells)
- Don't show project fork event as "imported" - Don't show project fork event as "imported"
- Add API endpoint to fetch merge request commits list - Add API endpoint to fetch merge request commits list
- Expose events API with comment information and author info - Expose events API with comment information and author info
- Fix: Ensure "Remove Source Branch" button is not shown when branch is being deleted. #3583 - Fix: Ensure "Remove Source Branch" button is not shown when branch is being deleted. #3583
- Fix 500 error when creating a merge request that removes a submodule
- Run custom Git hooks when branch is created or deleted.
- Fix bug when simultaneously accepting multiple MRs results in MRs that are of "merged" status, but not merged to the target branch
v 8.2.3 v 8.2.3
- Fix application settings cache not expiring after changes (Stan Hu) - Fix application settings cache not expiring after changes (Stan Hu)
- Fix Error 500s when creating global milestones with Unicode characters (Stan Hu)
v 8.2.3
- Webhook payload has an added, modified and removed properties for each commit
v 8.2.2 v 8.2.2
- Fix 404 in redirection after removing a project (Stan Hu) - Fix 404 in redirection after removing a project (Stan Hu)
......
...@@ -99,7 +99,7 @@ gem 'org-ruby', '~> 0.9.12' ...@@ -99,7 +99,7 @@ gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0' gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1' gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2' gem 'asciidoctor', '~> 1.5.2'
gem 'net-ssh', '~> 3.0.1' gem 'rouge', '~> 1.10.1'
# Diffs # Diffs
gem 'diffy', '~> 3.0.3' gem 'diffy', '~> 3.0.3'
...@@ -120,8 +120,8 @@ gem 'acts-as-taggable-on', '~> 3.4' ...@@ -120,8 +120,8 @@ gem 'acts-as-taggable-on', '~> 3.4'
# Background jobs # Background jobs
gem 'sinatra', '~> 1.4.4', require: nil gem 'sinatra', '~> 1.4.4', require: nil
gem 'sidekiq', '3.3.0' gem 'sidekiq', '~> 3.5.0'
gem 'sidetiq', '~> 0.6.3' gem 'sidekiq-cron', '~> 0.3.0'
# HTTP requests # HTTP requests
gem "httparty", '~> 0.13.3' gem "httparty", '~> 0.13.3'
...@@ -171,6 +171,7 @@ gem "underscore-rails", "~> 1.4.4" ...@@ -171,6 +171,7 @@ gem "underscore-rails", "~> 1.4.4"
# Sanitize user input # Sanitize user input
gem "sanitize", '~> 2.0' gem "sanitize", '~> 2.0'
gem 'babosa', '~> 1.0.2'
# Protect against bruteforcing # Protect against bruteforcing
gem "rack-attack", '~> 4.3.0' gem "rack-attack", '~> 4.3.0'
...@@ -204,6 +205,7 @@ gem 'raphael-rails', '~> 2.1.2' ...@@ -204,6 +205,7 @@ gem 'raphael-rails', '~> 2.1.2'
gem 'request_store', '~> 1.2.0' gem 'request_store', '~> 1.2.0'
gem 'select2-rails', '~> 3.5.9' gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1' gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
group :development do group :development do
gem "foreman" gem "foreman"
......
...@@ -73,6 +73,7 @@ GEM ...@@ -73,6 +73,7 @@ GEM
descendants_tracker (~> 0.0.4) descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0) ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
babosa (1.0.2)
bcrypt (3.1.10) bcrypt (3.1.10)
benchmark-ips (2.3.0) benchmark-ips (2.3.0)
better_errors (1.0.1) better_errors (1.0.1)
...@@ -116,8 +117,23 @@ GEM ...@@ -116,8 +117,23 @@ GEM
activemodel (>= 3.2.0) activemodel (>= 3.2.0)
activesupport (>= 3.2.0) activesupport (>= 3.2.0)
json (>= 1.7) json (>= 1.7)
celluloid (0.16.0) celluloid (0.17.2)
timers (~> 4.0.0) celluloid-essentials
celluloid-extras
celluloid-fsm
celluloid-pool
celluloid-supervision
timers (>= 4.1.1)
celluloid-essentials (0.20.5)
timers (>= 4.1.1)
celluloid-extras (0.20.5)
timers (>= 4.1.1)
celluloid-fsm (0.20.5)
timers (>= 4.1.1)
celluloid-pool (0.20.5)
timers (>= 4.1.1)
celluloid-supervision (0.20.5)
timers (>= 4.1.1)
charlock_holmes (0.7.3) charlock_holmes (0.7.3)
chunky_png (1.3.5) chunky_png (1.3.5)
cliver (0.3.2) cliver (0.3.2)
...@@ -369,7 +385,6 @@ GEM ...@@ -369,7 +385,6 @@ GEM
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
httpclient (2.7.0.1) httpclient (2.7.0.1)
i18n (0.7.0) i18n (0.7.0)
ice_cube (0.11.1)
ice_nine (0.11.1) ice_nine (0.11.1)
inflecto (0.0.2) inflecto (0.0.2)
ipaddress (0.8.0) ipaddress (0.8.0)
...@@ -640,6 +655,7 @@ GEM ...@@ -640,6 +655,7 @@ GEM
sexp_processor (~> 4.1) sexp_processor (~> 4.1)
rubyntlm (0.5.2) rubyntlm (0.5.2)
rubypants (0.2.0) rubypants (0.2.0)
rufus-scheduler (3.1.10)
rugged (0.23.3) rugged (0.23.3)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (2.1.0) sanitize (2.1.0)
...@@ -667,16 +683,15 @@ GEM ...@@ -667,16 +683,15 @@ GEM
rack rack
shoulda-matchers (2.8.0) shoulda-matchers (2.8.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
sidekiq (3.3.0) sidekiq (3.5.3)
celluloid (>= 0.16.0) celluloid (~> 0.17.2)
connection_pool (>= 2.0.0) connection_pool (~> 2.2, >= 2.2.0)
json json (~> 1.0)
redis (>= 3.0.6) redis (~> 3.2, >= 3.2.1)
redis-namespace (>= 1.3.1) redis-namespace (~> 1.5, >= 1.5.2)
sidetiq (0.6.3) sidekiq-cron (0.3.1)
celluloid (>= 0.14.1) rufus-scheduler (>= 2.0.24)
ice_cube (= 0.11.1) sidekiq (>= 2.17.3)
sidekiq (>= 3.0.0)
simple_oauth (0.1.9) simple_oauth (0.1.9)
simplecov (0.10.0) simplecov (0.10.0)
docile (~> 1.1.0) docile (~> 1.1.0)
...@@ -742,7 +757,7 @@ GEM ...@@ -742,7 +757,7 @@ GEM
thor (0.19.1) thor (0.19.1)
thread_safe (0.3.5) thread_safe (0.3.5)
tilt (1.4.1) tilt (1.4.1)
timers (4.0.4) timers (4.1.1)
hitimes hitimes
timfel-krb5-auth (0.8.3) timfel-krb5-auth (0.8.3)
tinder (1.10.1) tinder (1.10.1)
...@@ -823,6 +838,7 @@ DEPENDENCIES ...@@ -823,6 +838,7 @@ DEPENDENCIES
asciidoctor (~> 1.5.2) asciidoctor (~> 1.5.2)
attr_encrypted (~> 1.3.4) attr_encrypted (~> 1.3.4)
awesome_print (~> 1.2.0) awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
benchmark-ips benchmark-ips
better_errors (~> 1.0.1) better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2) binding_of_caller (~> 0.7.2)
...@@ -924,6 +940,7 @@ DEPENDENCIES ...@@ -924,6 +940,7 @@ DEPENDENCIES
request_store (~> 1.2.0) request_store (~> 1.2.0)
rerun (~> 0.10.0) rerun (~> 0.10.0)
responders (~> 2.0) responders (~> 2.0)
rouge (~> 1.10.1)
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.3.0) rspec-rails (~> 3.3.0)
rubocop (~> 0.28.0) rubocop (~> 0.28.0)
...@@ -936,8 +953,8 @@ DEPENDENCIES ...@@ -936,8 +953,8 @@ DEPENDENCIES
settingslogic (~> 2.0.9) settingslogic (~> 2.0.9)
sham_rack sham_rack
shoulda-matchers (~> 2.8.0) shoulda-matchers (~> 2.8.0)
sidekiq (= 3.3.0) sidekiq (~> 3.5.0)
sidetiq (~> 0.6.3) sidekiq-cron (~> 0.3.0)
simplecov (~> 0.10.0) simplecov (~> 0.10.0)
sinatra (~> 1.4.4) sinatra (~> 1.4.4)
six (~> 0.2.0) six (~> 0.2.0)
......
...@@ -80,7 +80,7 @@ There are a lot of [third-party applications integrating with GitLab](https://ab ...@@ -80,7 +80,7 @@ There are a lot of [third-party applications integrating with GitLab](https://ab
## GitLab release cycle ## GitLab release cycle
Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases are published when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG). For more information about the release process see the [release documentation](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/release). Features that will likely be in the next releases can be found on the [feature request forum](http://feedback.gitlab.com/forums/176466-general) with the status [started](http://feedback.gitlab.com/forums/176466-general/status/796456) and [completed](http://feedback.gitlab.com/forums/176466-general/status/796457). For more information about the release process see the [release documentation](http://doc.gitlab.com/ce/release/).
## Upgrading ## Upgrading
......
...@@ -88,4 +88,9 @@ class @AwardsHandler ...@@ -88,4 +88,9 @@ class @AwardsHandler
callback.call() callback.call()
findEmojiIcon: (emoji) -> findEmojiIcon: (emoji) ->
$(".icon[data-emoji='" + emoji + "']") $(".icon[data-emoji='" + emoji + "']")
\ No newline at end of file
scrollToAwards: ->
$('body, html').animate({
scrollTop: $('.awards').offset().top - 80
}, 200)
class @Flash class @Flash
constructor: (message, type)-> constructor: (message, type)->
flash = $(".flash-container") @flash = $(".flash-container")
flash.html("") @flash.html("")
$('<div/>', innerDiv = $('<div/>',
class: "flash-#{type}", class: "flash-#{type}",
text: message text: message
).appendTo(".flash-container") )
innerDiv.appendTo(".flash-container")
flash.click -> $(@).fadeOut() @flash.click -> $(@).fadeOut()
flash.show() @flash.show()
pin: ->
@flash.addClass('flash-pinned flash-raised')
...@@ -111,6 +111,12 @@ class @Notes ...@@ -111,6 +111,12 @@ class @Notes
Note: for rendering inline notes use renderDiscussionNote Note: for rendering inline notes use renderDiscussionNote
### ###
renderNote: (note) -> renderNote: (note) ->
unless note.valid
if note.award
flash = new Flash('You have already used this award emoji!', 'alert')
flash.pin()
return
# render note if it not present in loaded list # render note if it not present in loaded list
# or skip if rendered # or skip if rendered
if @isNewNote(note) && !note.award if @isNewNote(note) && !note.award
...@@ -122,6 +128,7 @@ class @Notes ...@@ -122,6 +128,7 @@ class @Notes
if note.award if note.award
awards_handler.addAwardToEmojiBar(note.note, note.emoji_path) awards_handler.addAwardToEmojiBar(note.note, note.emoji_path)
awards_handler.scrollToAwards()
### ###
Check if note does not exists on page Check if note does not exists on page
...@@ -362,8 +369,8 @@ class @Notes ...@@ -362,8 +369,8 @@ class @Notes
note = $(this).closest(".note") note = $(this).closest(".note")
note.find(".note-attachment").remove() note.find(".note-attachment").remove()
note.find(".note-body > .note-text").show() note.find(".note-body > .note-text").show()
note.find(".js-note-attachment-delete").hide() note.find(".note-header").show()
note.find(".note-edit-form").hide() note.find(".current-note-edit-form").remove()
### ###
Called when clicking on the "reply" button for a diff line. Called when clicking on the "reply" button for a diff line.
......
...@@ -15,3 +15,13 @@ ...@@ -15,3 +15,13 @@
@extend .alert-danger; @extend .alert-danger;
} }
} }
.flash-pinned {
position: fixed;
top: 80px;
width: 80%;
}
.flash-raised {
z-index: 1000;
}
...@@ -73,11 +73,8 @@ ...@@ -73,11 +73,8 @@
} }
.referenced-users { .referenced-users {
padding: 10px 0; color: #4c4e54;
color: #999; padding-top: 10px;
margin-left: 10px;
margin-top: 1px;
margin-right: 130px;
} }
.md-preview-holder { .md-preview-holder {
......
...@@ -2,8 +2,10 @@ module GlobalMilestones ...@@ -2,8 +2,10 @@ module GlobalMilestones
extend ActiveSupport::Concern extend ActiveSupport::Concern
def milestones def milestones
epoch = DateTime.parse('1970-01-01')
@milestones = MilestonesFinder.new.execute(@projects, params) @milestones = MilestonesFinder.new.execute(@projects, params)
@milestones = GlobalMilestone.build_collection(@milestones) @milestones = GlobalMilestone.build_collection(@milestones)
@milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
@milestones = Kaminari.paginate_array(@milestones).page(params[:page]).per(ApplicationController::PER_PAGE) @milestones = Kaminari.paginate_array(@milestones).page(params[:page]).per(ApplicationController::PER_PAGE)
end end
......
...@@ -46,7 +46,7 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -46,7 +46,7 @@ class Groups::MilestonesController < Groups::ApplicationController
end end
def milestone_path(title) def milestone_path(title)
group_milestone_path(@group, title.parameterize, title: title) group_milestone_path(@group, title.to_slug.to_s, title: title)
end end
def projects def projects
......
...@@ -25,13 +25,12 @@ class Projects::HooksController < Projects::ApplicationController ...@@ -25,13 +25,12 @@ class Projects::HooksController < Projects::ApplicationController
def test def test
if !@project.empty_repo? if !@project.empty_repo?
status = TestHookService.new.execute(hook, current_user) status, message = TestHookService.new.execute(hook, current_user)
if status if status
flash[:notice] = 'Hook successfully executed.' flash[:notice] = 'Hook successfully executed.'
else else
flash[:alert] = 'Hook execution failed. '\ flash[:alert] = "Hook execution failed: #{message}"
'Ensure hook URL is correct and service is up.'
end end
else else
flash[:alert] = 'Hook execution failed. Ensure the project has commits.' flash[:alert] = 'Hook execution failed. Ensure the project has commits.'
......
...@@ -131,16 +131,25 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -131,16 +131,25 @@ class Projects::NotesController < Projects::ApplicationController
end end
def render_note_json(note) def render_note_json(note)
render json: { if note.valid?
id: note.id, render json: {
discussion_id: note.discussion_id, valid: true,
html: note_to_html(note), id: note.id,
award: note.is_award, discussion_id: note.discussion_id,
emoji_path: note.is_award ? view_context.image_url(::AwardEmoji.path_to_emoji_image(note.note)) : "", html: note_to_html(note),
note: note.note, award: note.is_award,
discussion_html: note_to_discussion_html(note), emoji_path: note.is_award ? view_context.image_url(::AwardEmoji.path_to_emoji_image(note.note)) : "",
discussion_with_diff_html: note_to_discussion_with_diff_html(note) note: note.note,
} discussion_html: note_to_discussion_html(note),
discussion_with_diff_html: note_to_discussion_with_diff_html(note)
}
else
render json: {
valid: false,
award: note.is_award,
errors: note.errors
}
end
end end
def authorize_admin_note! def authorize_admin_note!
......
class MilestonesFinder class MilestonesFinder
def execute(projects, params) def execute(projects, params)
milestones = Milestone.of_projects(projects) milestones = Milestone.of_projects(projects)
milestones = milestones.order("due_date ASC") milestones = milestones.reorder("due_date ASC")
case params[:state] case params[:state]
when 'closed' then milestones.closed when 'closed' then milestones.closed
......
...@@ -209,7 +209,7 @@ module ApplicationHelper ...@@ -209,7 +209,7 @@ module ApplicationHelper
title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'), title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'),
data: { toggle: 'tooltip', placement: placement, container: 'body' } data: { toggle: 'tooltip', placement: placement, container: 'body' }
element += javascript_tag "$('.js-timeago').timeago()" unless skip_js element += javascript_tag "$('.js-timeago').last().timeago()" unless skip_js
element element
end end
......
...@@ -28,7 +28,9 @@ module MilestonesHelper ...@@ -28,7 +28,9 @@ module MilestonesHelper
Milestone.where(project_id: @projects) Milestone.where(project_id: @projects)
end.active end.active
epoch = DateTime.parse('1970-01-01')
grouped_milestones = GlobalMilestone.build_collection(milestones) grouped_milestones = GlobalMilestone.build_collection(milestones)
grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
grouped_milestones.unshift(Milestone::None) grouped_milestones.unshift(Milestone::None)
grouped_milestones.unshift(Milestone::Any) grouped_milestones.unshift(Milestone::Any)
......
...@@ -43,12 +43,12 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -43,12 +43,12 @@ class ApplicationSetting < ActiveRecord::Base
validates :home_page_url, validates :home_page_url,
allow_blank: true, allow_blank: true,
format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, url: true,
if: :home_page_url_column_exist if: :home_page_url_column_exist
validates :after_sign_out_path, validates :after_sign_out_path,
allow_blank: true, allow_blank: true,
format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" } url: true
validates :admin_notification_email, validates :admin_notification_email,
allow_blank: true, allow_blank: true,
......
...@@ -16,12 +16,12 @@ ...@@ -16,12 +16,12 @@
class BroadcastMessage < ActiveRecord::Base class BroadcastMessage < ActiveRecord::Base
include Sortable include Sortable
validates :message, presence: true validates :message, presence: true
validates :starts_at, presence: true validates :starts_at, presence: true
validates :ends_at, presence: true validates :ends_at, presence: true
validates :color, format: { with: /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/ }, allow_blank: true validates :color, allow_blank: true, color: true
validates :font, format: { with: /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/ }, allow_blank: true validates :font, allow_blank: true, color: true
def self.current def self.current
where("ends_at > :now AND starts_at < :now", now: Time.zone.now).last where("ends_at > :now AND starts_at < :now", now: Time.zone.now).last
......
...@@ -20,8 +20,7 @@ module Ci ...@@ -20,8 +20,7 @@ module Ci
# HTTParty timeout # HTTParty timeout
default_timeout 10 default_timeout 10
validates :url, presence: true, validates :url, presence: true, url: true
format: { with: URI::regexp(%w(http https)), message: "should be a valid url" }
def execute(data) def execute(data)
parsed_url = URI.parse(url) parsed_url = URI.parse(url)
......
...@@ -147,10 +147,10 @@ class Commit ...@@ -147,10 +147,10 @@ class Commit
description.present? description.present?
end end
def hook_attrs def hook_attrs(with_changed_files: false)
path_with_namespace = project.path_with_namespace path_with_namespace = project.path_with_namespace
{ data = {
id: id, id: id,
message: safe_message, message: safe_message,
timestamp: committed_date.xmlschema, timestamp: committed_date.xmlschema,
...@@ -160,6 +160,12 @@ class Commit ...@@ -160,6 +160,12 @@ class Commit
email: author_email email: author_email
} }
} }
if with_changed_files
data.merge!(repo_changes)
end
data
end end
# Discover issues should be closed when this commit is pushed to a project's # Discover issues should be closed when this commit is pushed to a project's
...@@ -208,4 +214,22 @@ class Commit ...@@ -208,4 +214,22 @@ class Commit
def status def status
ci_commit.try(:status) || :not_found ci_commit.try(:status) || :not_found
end end
private
def repo_changes
changes = { added: [], modified: [], removed: [] }
diffs.each do |diff|
if diff.deleted_file
changes[:removed] << diff.old_path
elsif diff.renamed_file || diff.new_file
changes[:added] << diff.new_path
else
changes[:modified] << diff.new_path
end
end
changes
end
end end
...@@ -16,7 +16,15 @@ class GlobalMilestone ...@@ -16,7 +16,15 @@ class GlobalMilestone
end end
def safe_title def safe_title
@title.parameterize @title.to_slug.to_s
end
def expired?
if due_date
due_date.past?
else
false
end
end end
def projects def projects
...@@ -98,4 +106,25 @@ class GlobalMilestone ...@@ -98,4 +106,25 @@ class GlobalMilestone
def complete? def complete?
total_items_count == closed_items_count total_items_count == closed_items_count
end end
def due_date
return @due_date if defined?(@due_date)
@due_date =
if @milestones.all? { |x| x.due_date == @milestones.first.due_date }
@milestones.first.due_date
else
nil
end
end
def expires_at
if due_date
if due_date.past?
"expired at #{due_date.stamp("Aug 21, 2011")}"
else
"expires at #{due_date.stamp("Aug 21, 2011")}"
end
end
end
end end
...@@ -31,37 +31,38 @@ class WebHook < ActiveRecord::Base ...@@ -31,37 +31,38 @@ class WebHook < ActiveRecord::Base
# HTTParty timeout # HTTParty timeout
default_timeout Gitlab.config.gitlab.webhook_timeout default_timeout Gitlab.config.gitlab.webhook_timeout
validates :url, presence: true, validates :url, presence: true, url: true
format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }
def execute(data, hook_name) def execute(data, hook_name)
parsed_url = URI.parse(url) parsed_url = URI.parse(url)
if parsed_url.userinfo.blank? if parsed_url.userinfo.blank?
WebHook.post(url, response = WebHook.post(url,
body: data.to_json, body: data.to_json,
headers: { headers: {
"Content-Type" => "application/json", "Content-Type" => "application/json",
"X-Gitlab-Event" => hook_name.singularize.titleize "X-Gitlab-Event" => hook_name.singularize.titleize
}, },
verify: enable_ssl_verification) verify: enable_ssl_verification)
else else
post_url = url.gsub("#{parsed_url.userinfo}@", "") post_url = url.gsub("#{parsed_url.userinfo}@", "")
auth = { auth = {
username: URI.decode(parsed_url.user), username: URI.decode(parsed_url.user),
password: URI.decode(parsed_url.password), password: URI.decode(parsed_url.password),
} }
WebHook.post(post_url, response = WebHook.post(post_url,
body: data.to_json, body: data.to_json,
headers: { headers: {
"Content-Type" => "application/json", "Content-Type" => "application/json",
"X-Gitlab-Event" => hook_name.singularize.titleize "X-Gitlab-Event" => hook_name.singularize.titleize
}, },
verify: enable_ssl_verification, verify: enable_ssl_verification,
basic_auth: auth) basic_auth: auth)
end end
rescue SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
[response.code == 200, ActionView::Base.full_sanitizer.sanitize(response.to_s)]
rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
logger.error("WebHook Error => #{e}") logger.error("WebHook Error => #{e}")
false [false, e.to_s]
end end
def async_execute(data, hook_name) def async_execute(data, hook_name)
......
...@@ -27,9 +27,7 @@ class Label < ActiveRecord::Base ...@@ -27,9 +27,7 @@ class Label < ActiveRecord::Base
has_many :label_links, dependent: :destroy has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue' has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
validates :color, validates :color, color: true, allow_blank: false
format: { with: /\A#[0-9A-Fa-f]{6}\Z/ },
allow_blank: false
validates :project, presence: true, unless: Proc.new { |service| service.template? } validates :project, presence: true, unless: Proc.new { |service| service.template? }
# Don't allow '?', '&', and ',' for label titles # Don't allow '?', '&', and ',' for label titles
......
...@@ -312,7 +312,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -312,7 +312,7 @@ class MergeRequest < ActiveRecord::Base
work_in_progress: work_in_progress? work_in_progress: work_in_progress?
} }
unless last_commit.nil? if last_commit
attrs.merge!(last_commit: last_commit.hook_attrs) attrs.merge!(last_commit: last_commit.hook_attrs)
end end
......
...@@ -23,19 +23,17 @@ class Namespace < ActiveRecord::Base ...@@ -23,19 +23,17 @@ class Namespace < ActiveRecord::Base
validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name, validates :name,
presence: true, uniqueness: true,
length: { within: 0..255 }, length: { within: 0..255 },
format: { with: Gitlab::Regex.namespace_name_regex, namespace_name: true,
message: Gitlab::Regex.namespace_name_regex_message } presence: true,
uniqueness: true
validates :description, length: { within: 0..255 } validates :description, length: { within: 0..255 }
validates :path, validates :path,
uniqueness: { case_sensitive: false },
presence: true,
length: { within: 1..255 }, length: { within: 1..255 },
exclusion: { in: Gitlab::Blacklist.path }, namespace: true,
format: { with: Gitlab::Regex.namespace_regex, presence: true,
message: Gitlab::Regex.namespace_regex_message } uniqueness: { case_sensitive: false }
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
......
...@@ -39,9 +39,12 @@ class Note < ActiveRecord::Base ...@@ -39,9 +39,12 @@ class Note < ActiveRecord::Base
delegate :name, to: :project, prefix: true delegate :name, to: :project, prefix: true
delegate :name, :email, to: :author, prefix: true delegate :name, :email, to: :author, prefix: true
before_validation :set_award!
validates :note, :project, presence: true validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award } validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true validates :note, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award }
validates :line_code, line_code: true, allow_blank: true
# Attachments are deprecated and are handled by Markdown uploader # Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size } validates :attachment, file_size: { maximum: :max_attachment_size }
...@@ -348,4 +351,31 @@ class Note < ActiveRecord::Base ...@@ -348,4 +351,31 @@ class Note < ActiveRecord::Base
def editable? def editable?
!system? !system?
end end
# Checks if note is an award added as a comment
#
# If note is an award, this method sets is_award to true
# and changes content of the note to award name.
#
# Method is executed as a before_validation callback.
#
def set_award!
return unless awards_supported? && contains_emoji_only?
self.is_award = true
self.note = award_emoji_name
end
private
def awards_supported?
noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest)
end
def contains_emoji_only?
note =~ /\A#{Gitlab::Markdown::EmojiFilter.emoji_pattern}\s?\Z/
end
def award_emoji_name
note.match(Gitlab::Markdown::EmojiFilter.emoji_pattern)[1]
end
end end
...@@ -152,7 +152,7 @@ class Project < ActiveRecord::Base ...@@ -152,7 +152,7 @@ class Project < ActiveRecord::Base
validates_uniqueness_of :name, scope: :namespace_id validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id validates_uniqueness_of :path, scope: :namespace_id
validates :import_url, validates :import_url,
format: { with: /\A#{URI.regexp(%w(ssh git http https))}\z/, message: 'should be a valid url' }, url: { protocols: %w(ssh git http https) },
if: :external_import? if: :external_import?
validates :star_count, numericality: { greater_than_or_equal_to: 0 } validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create validate :check_limit, on: :create
......
...@@ -23,10 +23,7 @@ class BambooService < CiService ...@@ -23,10 +23,7 @@ class BambooService < CiService
prop_accessor :bamboo_url, :build_key, :username, :password prop_accessor :bamboo_url, :build_key, :username, :password
validates :bamboo_url, validates :bamboo_url, presence: true, url: true, if: :activated?
presence: true,
format: { with: /\A#{URI.regexp}\z/ },
if: :activated?
validates :build_key, presence: true, if: :activated? validates :build_key, presence: true, if: :activated?
validates :username, validates :username,
presence: true, presence: true,
...@@ -84,7 +81,7 @@ class BambooService < CiService ...@@ -84,7 +81,7 @@ class BambooService < CiService
def supported_events def supported_events
%w(push) %w(push)
end end
def build_info(sha) def build_info(sha)
url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}") url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}")
......
...@@ -19,14 +19,11 @@ ...@@ -19,14 +19,11 @@
# #
class DroneCiService < CiService class DroneCiService < CiService
prop_accessor :drone_url, :token, :enable_ssl_verification prop_accessor :drone_url, :token, :enable_ssl_verification
validates :drone_url,
presence: true, validates :drone_url, presence: true, url: true, if: :activated?
format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, if: :activated? validates :token, presence: true, if: :activated?
validates :token,
presence: true,
if: :activated?
after_save :compose_service_hook, if: :activated? after_save :compose_service_hook, if: :activated?
...@@ -58,16 +55,16 @@ class DroneCiService < CiService ...@@ -58,16 +55,16 @@ class DroneCiService < CiService
end end
def merge_request_status_path(iid, sha = nil, ref = nil) def merge_request_status_path(iid, sha = nil, ref = nil)
url = [drone_url, url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}", "gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}",
"?access_token=#{token}"] "?access_token=#{token}"]
URI.join(*url).to_s URI.join(*url).to_s
end end
def commit_status_path(sha, ref) def commit_status_path(sha, ref)
url = [drone_url, url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}", "gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}",
"?branch=#{URI::encode(ref.to_s)}&access_token=#{token}"] "?branch=#{URI::encode(ref.to_s)}&access_token=#{token}"]
URI.join(*url).to_s URI.join(*url).to_s
...@@ -114,15 +111,15 @@ class DroneCiService < CiService ...@@ -114,15 +111,15 @@ class DroneCiService < CiService
end end
def merge_request_page(iid, sha, ref) def merge_request_page(iid, sha, ref)
url = [drone_url, url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/redirect/pulls/#{iid}"] "gitlab/#{project.namespace.path}/#{project.path}/redirect/pulls/#{iid}"]
URI.join(*url).to_s URI.join(*url).to_s
end end
def commit_page(sha, ref) def commit_page(sha, ref)
url = [drone_url, url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}", "gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}",
"?branch=#{URI::encode(ref.to_s)}"] "?branch=#{URI::encode(ref.to_s)}"]
URI.join(*url).to_s URI.join(*url).to_s
...@@ -163,10 +160,10 @@ class DroneCiService < CiService ...@@ -163,10 +160,10 @@ class DroneCiService < CiService
end end
def push_valid?(data) def push_valid?(data)
opened_merge_requests = project.merge_requests.opened.where(source_project_id: project.id, opened_merge_requests = project.merge_requests.opened.where(source_project_id: project.id,
source_branch: Gitlab::Git.ref_name(data[:ref])) source_branch: Gitlab::Git.ref_name(data[:ref]))
opened_merge_requests.empty? && data[:total_commits_count] > 0 && opened_merge_requests.empty? && data[:total_commits_count] > 0 &&
!Gitlab::Git.blank_ref?(data[:after]) !Gitlab::Git.blank_ref?(data[:after])
end end
......
...@@ -22,10 +22,8 @@ class ExternalWikiService < Service ...@@ -22,10 +22,8 @@ class ExternalWikiService < Service
include HTTParty include HTTParty
prop_accessor :external_wiki_url prop_accessor :external_wiki_url
validates :external_wiki_url,
presence: true, validates :external_wiki_url, presence: true, url: true, if: :activated?
format: { with: /\A#{URI.regexp}\z/ },
if: :activated?
def title def title
'External Wiki' 'External Wiki'
......
...@@ -23,16 +23,16 @@ class TeamcityService < CiService ...@@ -23,16 +23,16 @@ class TeamcityService < CiService
prop_accessor :teamcity_url, :build_type, :username, :password prop_accessor :teamcity_url, :build_type, :username, :password
validates :teamcity_url, validates :teamcity_url, presence: true, url: true, if: :activated?
presence: true,
format: { with: /\A#{URI.regexp}\z/ }, if: :activated?
validates :build_type, presence: true, if: :activated? validates :build_type, presence: true, if: :activated?
validates :username, validates :username,
presence: true, presence: true,
if: ->(service) { service.password? }, if: :activated? if: ->(service) { service.password? },
if: :activated?
validates :password, validates :password,
presence: true, presence: true,
if: ->(service) { service.username? }, if: :activated? if: ->(service) { service.username? },
if: :activated?
attr_accessor :response attr_accessor :response
......
require 'securerandom' require 'securerandom'
class Repository class Repository
class PreReceiveError < StandardError; end
class CommitError < StandardError; end class CommitError < StandardError; end
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
...@@ -101,17 +100,26 @@ class Repository ...@@ -101,17 +100,26 @@ class Repository
end end
def find_branch(name) def find_branch(name)
branches.find { |branch| branch.name == name } raw_repository.branches.find { |branch| branch.name == name }
end end
def find_tag(name) def find_tag(name)
tags.find { |tag| tag.name == name } raw_repository.tags.find { |tag| tag.name == name }
end end
def add_branch(branch_name, ref) def add_branch(user, branch_name, target)
expire_branches_cache oldrev = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
target = commit(target).try(:id)
return false unless target
GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do
rugged.branches.create(branch_name, target)
end
gitlab_shell.add_branch(path_with_namespace, branch_name, ref) expire_branches_cache
find_branch(branch_name)
end end
def add_tag(tag_name, ref, message = nil) def add_tag(tag_name, ref, message = nil)
...@@ -120,10 +128,20 @@ class Repository ...@@ -120,10 +128,20 @@ class Repository
gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message) gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message)
end end
def rm_branch(branch_name) def rm_branch(user, branch_name)
expire_branches_cache expire_branches_cache
gitlab_shell.rm_branch(path_with_namespace, branch_name) branch = find_branch(branch_name)
oldrev = branch.try(:target)
newrev = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do
rugged.branches.delete(branch_name)
end
expire_branches_cache
true
end end
def rm_tag(tag_name) def rm_tag(tag_name)
...@@ -550,7 +568,6 @@ class Repository ...@@ -550,7 +568,6 @@ class Repository
def commit_with_hooks(current_user, branch) def commit_with_hooks(current_user, branch)
oldrev = Gitlab::Git::BLANK_SHA oldrev = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
gl_id = Gitlab::ShellEnv.gl_id(current_user)
was_empty = empty? was_empty = empty?
# Create temporary ref # Create temporary ref
...@@ -569,15 +586,7 @@ class Repository ...@@ -569,15 +586,7 @@ class Repository
raise CommitError.new('Failed to create commit') raise CommitError.new('Failed to create commit')
end end
# Run GitLab pre-receive hook GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', path_to_repo)
pre_receive_hook_status = pre_receive_hook.trigger(gl_id, oldrev, newrev, ref)
# Run GitLab update hook
update_hook = Gitlab::Git::Hook.new('update', path_to_repo)
update_hook_status = update_hook.trigger(gl_id, oldrev, newrev, ref)
if pre_receive_hook_status && update_hook_status
if was_empty if was_empty
# Create branch # Create branch
rugged.references.create(ref, newrev) rugged.references.create(ref, newrev)
...@@ -592,16 +601,11 @@ class Repository ...@@ -592,16 +601,11 @@ class Repository
raise CommitError.new('Commit was rejected because branch received new push') raise CommitError.new('Commit was rejected because branch received new push')
end end
end end
# Run GitLab post receive hook
post_receive_hook = Gitlab::Git::Hook.new('post-receive', path_to_repo)
post_receive_hook.trigger(gl_id, oldrev, newrev, ref)
else
# Remove tmp ref and return error to user
rugged.references.delete(tmp_ref)
raise PreReceiveError.new('Commit was rejected by git hook')
end end
rescue GitHooksService::PreReceiveError
# Remove tmp ref and return error to user
rugged.references.delete(tmp_ref)
raise
end end
private private
......
...@@ -21,7 +21,7 @@ class SentNotification < ActiveRecord::Base ...@@ -21,7 +21,7 @@ class SentNotification < ActiveRecord::Base
validates :reply_key, uniqueness: true validates :reply_key, uniqueness: true
validates :noteable_id, presence: true, unless: :for_commit? validates :noteable_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit? validates :commit_id, presence: true, if: :for_commit?
validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true validates :line_code, line_code: true, allow_blank: true
class << self class << self
def reply_key def reply_key
......
...@@ -148,11 +148,9 @@ class User < ActiveRecord::Base ...@@ -148,11 +148,9 @@ class User < ActiveRecord::Base
validates :bio, length: { maximum: 255 }, allow_blank: true validates :bio, length: { maximum: 255 }, allow_blank: true
validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :username, validates :username,
namespace: true,
presence: true, presence: true,
uniqueness: { case_sensitive: false }, uniqueness: { case_sensitive: false }
exclusion: { in: Gitlab::Blacklist.path },
format: { with: Gitlab::Regex.namespace_regex,
message: Gitlab::Regex.namespace_regex_message }
validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true
validate :namespace_uniq, if: ->(user) { user.username_changed? } validate :namespace_uniq, if: ->(user) { user.username_changed? }
......
...@@ -13,8 +13,7 @@ class CreateBranchService < BaseService ...@@ -13,8 +13,7 @@ class CreateBranchService < BaseService
return error('Branch already exists') return error('Branch already exists')
end end
repository.add_branch(branch_name, ref) new_branch = repository.add_branch(current_user, branch_name, ref)
new_branch = repository.find_branch(branch_name)
if new_branch if new_branch
push_data = build_push_data(project, current_user, new_branch) push_data = build_push_data(project, current_user, new_branch)
...@@ -27,6 +26,8 @@ class CreateBranchService < BaseService ...@@ -27,6 +26,8 @@ class CreateBranchService < BaseService
else else
error('Invalid reference name') error('Invalid reference name')
end end
rescue GitHooksService::PreReceiveError
error('Branch creation was rejected by Git hook')
end end
def success(branch) def success(branch)
......
...@@ -24,7 +24,7 @@ class DeleteBranchService < BaseService ...@@ -24,7 +24,7 @@ class DeleteBranchService < BaseService
return error('You dont have push access to repo', 405) return error('You dont have push access to repo', 405)
end end
if repository.rm_branch(branch_name) if repository.rm_branch(current_user, branch_name)
push_data = build_push_data(branch) push_data = build_push_data(branch)
EventCreateService.new.push(project, current_user, push_data) EventCreateService.new.push(project, current_user, push_data)
...@@ -35,6 +35,8 @@ class DeleteBranchService < BaseService ...@@ -35,6 +35,8 @@ class DeleteBranchService < BaseService
else else
error('Failed to remove branch') error('Failed to remove branch')
end end
rescue GitHooksService::PreReceiveError
error('Branch deletion was rejected by Git hook')
end end
def error(message, return_code = 400) def error(message, return_code = 400)
......
...@@ -26,7 +26,7 @@ module Files ...@@ -26,7 +26,7 @@ module Files
else else
error("Something went wrong. Your changes were not committed") error("Something went wrong. Your changes were not committed")
end end
rescue Repository::CommitError, Repository::PreReceiveError, ValidationError => ex rescue Repository::CommitError, GitHooksService::PreReceiveError, ValidationError => ex
error(ex.message) error(ex.message)
end end
......
class GitHooksService
PreReceiveError = Class.new(StandardError)
def execute(user, repo_path, oldrev, newrev, ref)
@repo_path = repo_path
@user = Gitlab::ShellEnv.gl_id(user)
@oldrev = oldrev
@newrev = newrev
@ref = ref
%w(pre-receive update).each do |hook_name|
unless run_hook(hook_name)
raise PreReceiveError.new("Git operation was rejected by #{hook_name} hook")
end
end
yield
run_hook('post-receive')
end
private
def run_hook(name)
hook = Gitlab::Git::Hook.new(name, @repo_path)
hook.trigger(@user, @oldrev, @newrev, @ref)
end
end
...@@ -5,11 +5,6 @@ module Notes ...@@ -5,11 +5,6 @@ module Notes
note.author = current_user note.author = current_user
note.system = false note.system = false
if contains_emoji_only?(params[:note])
note.is_award = true
note.note = emoji_name(params[:note])
end
if note.save if note.save
notification_service.new_note(note) notification_service.new_note(note)
...@@ -33,13 +28,5 @@ module Notes ...@@ -33,13 +28,5 @@ module Notes
note.project.execute_hooks(note_data, :note_hooks) note.project.execute_hooks(note_data, :note_hooks)
note.project.execute_services(note_data, :note_hooks) note.project.execute_services(note_data, :note_hooks)
end end
def contains_emoji_only?(note)
note =~ /\A:[-_+[:alnum:]]*:\s?\z/
end
def emoji_name(note)
note.match(/\A:([-_+[:alnum:]]*):\s?/)[1]
end
end end
end end
# ColorValidator
#
# Custom validator for web color codes. It requires the leading hash symbol and
# will accept RGB triplet or hexadecimal formats.
#
# Example:
#
# class User < ActiveRecord::Base
# validates :background_color, allow_blank: true, color: true
# end
#
class ColorValidator < ActiveModel::EachValidator
PATTERN = /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/.freeze
def validate_each(record, attribute, value)
unless value =~ PATTERN
record.errors.add(attribute, "must be a valid color code")
end
end
end
# EmailValidator
#
# Based on https://github.com/balexand/email_validator # Based on https://github.com/balexand/email_validator
# #
# Extended to use only strict mode with following allowed characters: # Extended to use only strict mode with following allowed characters:
...@@ -6,15 +8,10 @@ ...@@ -6,15 +8,10 @@
# See http://www.remote.org/jochen/mail/info/chars.html # See http://www.remote.org/jochen/mail/info/chars.html
# #
class EmailValidator < ActiveModel::EachValidator class EmailValidator < ActiveModel::EachValidator
@@default_options = {} PATTERN = /\A\s*([-a-z0-9+._']{1,64})@((?:[-a-z0-9]+\.)+[a-z]{2,})\s*\z/i.freeze
def self.default_options
@@default_options
end
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
options = @@default_options.merge(self.options) unless value =~ PATTERN
unless value =~ /\A\s*([-a-z0-9+._']{1,64})@((?:[-a-z0-9]+\.)+[a-z]{2,})\s*\z/i
record.errors.add(attribute, options[:message] || :invalid) record.errors.add(attribute, options[:message] || :invalid)
end end
end end
......
# LineCodeValidator
#
# Custom validator for GitLab line codes.
class LineCodeValidator < ActiveModel::EachValidator
PATTERN = /\A[a-z0-9]+_\d+_\d+\z/.freeze
def validate_each(record, attribute, value)
unless value =~ PATTERN
record.errors.add(attribute, "must be a valid line code")
end
end
end
# NamespaceNameValidator
#
# Custom validator for GitLab namespace name strings.
class NamespaceNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value =~ Gitlab::Regex.namespace_name_regex
record.errors.add(attribute, Gitlab::Regex.namespace_name_regex_message)
end
end
end
# NamespaceValidator
#
# Custom validator for GitLab namespace values.
#
# Values are checked for formatting and exclusion from a list of reserved path
# names.
class NamespaceValidator < ActiveModel::EachValidator
RESERVED = %w(
admin
all
assets
ci
dashboard
files
groups
help
hooks
issues
merge_requests
notes
profile
projects
public
repository
s
search
services
snippets
teams
u
unsubscribes
users
).freeze
def validate_each(record, attribute, value)
unless value =~ Gitlab::Regex.namespace_regex
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
end
if reserved?(value)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
private
def reserved?(value)
RESERVED.include?(value)
end
end
# UrlValidator
#
# Custom validator for URLs.
#
# By default, only URLs for the HTTP(S) protocols will be considered valid.
# Provide a `:protocols` option to configure accepted protocols.
#
# Example:
#
# class User < ActiveRecord::Base
# validates :personal_url, url: true
#
# validates :ftp_url, url: { protocols: %w(ftp) }
#
# validates :git_url, url: { protocols: %w(http https ssh git) }
# end
#
class UrlValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless valid_url?(value)
record.errors.add(attribute, "must be a valid URL")
end
end
private
def default_options
@default_options ||= { protocols: %w(http https) }
end
def valid_url?(value)
options = default_options.merge(self.options)
value =~ /\A#{URI.regexp(options[:protocols])}\z/
end
end
...@@ -16,7 +16,10 @@ ...@@ -16,7 +16,10 @@
= milestone_progress_bar(milestone) = milestone_progress_bar(milestone)
.row .row
.col-sm-6 .col-sm-6
- milestone.milestones.each do |milestone| .expiration
= link_to milestone_path(milestone) do = render 'shared/milestone_expired', milestone: milestone
%span.label.label-gray .projects
= milestone.project.name_with_namespace - milestone.milestones.each do |milestone|
= link_to milestone_path(milestone) do
%span.label.label-gray
= milestone.project.name_with_namespace
...@@ -8,17 +8,18 @@ ...@@ -8,17 +8,18 @@
%a.js-md-preview-button(href="#md-preview-holder" tabindex="-1") %a.js-md-preview-button(href="#md-preview-holder" tabindex="-1")
Preview Preview
- if defined?(referenced_users) && referenced_users %div
%span.referenced-users.pull-left.hide .md-write-holder
= yield
.md.md-preview-holder.hide
.js-md-preview{class: (preview_class if defined?(preview_class))}
- if defined?(referenced_users) && referenced_users
%div.referenced-users.hide
%span
= icon('exclamation-triangle') = icon('exclamation-triangle')
You are about to add You are about to add
%strong %strong
%span.js-referenced-users-count 0 %span.js-referenced-users-count 0
people people
to the discussion. Proceed with caution. to the discussion. Proceed with caution.
%div
.md-write-holder
= yield
.md.md-preview-holder.hide
.js-md-preview{class: (preview_class if defined?(preview_class))}
...@@ -3,9 +3,8 @@ ...@@ -3,9 +3,8 @@
- if diff_file.diff.submodule? - if diff_file.diff.submodule?
%span %span
= icon('archive fw') = icon('archive fw')
- submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path)
%strong %strong
= submodule_link(submodule_item, @commit.id, project.repository) = submodule_link(blob, @commit.id, project.repository)
- else - else
%span %span
= blob_icon blob.mode, blob.name = blob_icon blob.mode, blob.name
......
...@@ -18,11 +18,7 @@ ...@@ -18,11 +18,7 @@
.row .row
.col-sm-6 .col-sm-6
- if milestone.expired? and not milestone.closed? = render 'shared/milestone_expired', milestone: milestone
%span.cred (Expired)
- if milestone.expires_at
%span
= milestone.expires_at
.col-sm-6 .col-sm-6
- if can?(current_user, :admin_milestone, milestone.project) and milestone.active? - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
= link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs edit-milestone-link btn-grouped" do = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs edit-milestone-link btn-grouped" do
......
- if milestone.expired? and not milestone.closed?
%span.cred (Expired)
- if milestone.expires_at
%span
= milestone.expires_at
class StuckCiBuildsWorker class StuckCiBuildsWorker
include Sidekiq::Worker include Sidekiq::Worker
include Sidetiq::Schedulable
BUILD_STUCK_TIMEOUT = 1.day BUILD_STUCK_TIMEOUT = 1.day
recurrence { daily }
def perform def perform
Rails.logger.info 'Cleaning stuck builds' Rails.logger.info 'Cleaning stuck builds'
......
...@@ -17,6 +17,12 @@ Sidekiq.configure_server do |config| ...@@ -17,6 +17,12 @@ Sidekiq.configure_server do |config|
chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS'] chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS']
chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS']
end end
# Sidekiq-cron: load recurring jobs from schedule.yml
schedule_file = 'config/schedule.yml'
if File.exists?(schedule_file)
Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file)
end
end end
Sidekiq.configure_client do |config| Sidekiq.configure_client do |config|
......
require 'sidekiq/web' require 'sidekiq/web'
require 'sidekiq/cron/web'
require 'api/api' require 'api/api'
Rails.application.routes.draw do Rails.application.routes.draw do
...@@ -368,7 +369,7 @@ Rails.application.routes.draw do ...@@ -368,7 +369,7 @@ Rails.application.routes.draw do
end end
resource :avatar, only: [:destroy] resource :avatar, only: [:destroy]
resources :milestones, only: [:index, :show, :update, :new, :create] resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
end end
end end
......
# Here is a list of jobs that are scheduled to run periodically.
# We use a UNIX cron notation to specify execution schedule.
#
# Please read here for more information:
# https://github.com/ondrejbartas/sidekiq-cron#adding-cron-job
stuck_ci_builds_worker:
cron: "0 0 * * *"
class: "StuckCiBuildsWorker"
queue: "default"
...@@ -190,7 +190,7 @@ This will create two service containers (MySQL and PostgreSQL). ...@@ -190,7 +190,7 @@ This will create two service containers (MySQL and PostgreSQL).
1. Create a build container and execute script in its context: 1. Create a build container and execute script in its context:
``` ```
$ cat build_script | docker run -n build -i -l mysql:service-mysql -l postgres:service-postgres ruby:2.1 /bin/bash $ docker run --name build -i --link=service-mysql:mysql --link=service-postgres:postgres ruby:2.1 /bin/bash < build_script
``` ```
This will create build container that has two service containers linked. This will create build container that has two service containers linked.
The build_script is piped using STDIN to bash interpreter which executes the build script in container. The build_script is piped using STDIN to bash interpreter which executes the build script in container.
......
GitLab has the following updates: ## Release cycle
Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases are published when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG). Features that will likely be in the next releases can be found on the [direction page](https://about.gitlab.com/direction/).
## Release process documentation
- [Monthly release](monthly.md), every month on the 22nd. - [Monthly release](monthly.md), every month on the 22nd.
- [Patch release](patch.md), if there are serious regressions. - [Patch release](patch.md), if there are serious regressions.
......
...@@ -57,6 +57,9 @@ X-Gitlab-Event: Push Hook ...@@ -57,6 +57,9 @@ X-Gitlab-Event: Push Hook
"name": "Jordi Mallach", "name": "Jordi Mallach",
"email": "jordi@softcatala.org" "email": "jordi@softcatala.org"
} }
"added": ["CHANGELOG"],
"modified": ["app/controller/application.rb"],
"removed": []
}, },
{ {
"id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
...@@ -66,13 +69,14 @@ X-Gitlab-Event: Push Hook ...@@ -66,13 +69,14 @@ X-Gitlab-Event: Push Hook
"author": { "author": {
"name": "GitLab dev user", "name": "GitLab dev user",
"email": "gitlabdev@dv6700.(none)" "email": "gitlabdev@dv6700.(none)"
} },
"added": ["CHANGELOG"],
"modified": ["app/controller/application.rb"],
"removed": []
} }
], ],
"total_commits_count": 4, "total_commits_count": 4
"added": ["CHANGELOG"],
"modified": ["app/controller/application.rb"],
"removed": []
} }
``` ```
......
...@@ -13,6 +13,12 @@ Feature: Project Commits Diff Comments ...@@ -13,6 +13,12 @@ Feature: Project Commits Diff Comments
Given I leave a diff comment like "Typo, please fix" Given I leave a diff comment like "Typo, please fix"
Then I should see a diff comment saying "Typo, please fix" Then I should see a diff comment saying "Typo, please fix"
@javascript
Scenario: I can add a diff comment with a single emoji
Given I open a diff comment form
And I write a diff comment like ":smile:"
Then I should see a diff comment with an emoji image
@javascript @javascript
Scenario: I get a temporary form for the first comment on a diff line Scenario: I get a temporary form for the first comment on a diff line
Given I open a diff comment form Given I open a diff comment form
......
...@@ -11,4 +11,8 @@ Feature: Award Emoji ...@@ -11,4 +11,8 @@ Feature: Award Emoji
And I click to emoji in the picker And I click to emoji in the picker
Then I have award added Then I have award added
And I can remove it by clicking to icon And I can remove it by clicking to icon
\ No newline at end of file @javascript
Scenario: I add award emoji using regular comment
Given I leave comment with a single emoji
Then I have award added
...@@ -8,10 +8,12 @@ Feature: Project Merge Requests Acceptance ...@@ -8,10 +8,12 @@ Feature: Project Merge Requests Acceptance
Given I am on the Merge Request detail page Given I am on the Merge Request detail page
When I click on "Remove source branch" option When I click on "Remove source branch" option
And I click on Accept Merge Request And I click on Accept Merge Request
Then I should not see the Remove Source Branch button Then I should see merge request merged
And I should not see the Remove Source Branch button
@javascript @javascript
Scenario: Accepting the Merge Request without removing the source branch Scenario: Accepting the Merge Request without removing the source branch
Given I am on the Merge Request detail page Given I am on the Merge Request detail page
When I click on Accept Merge Request When I click on Accept Merge Request
Then I should see the Remove Source Branch button Then I should see merge request merged
And I should see the Remove Source Branch button
...@@ -71,7 +71,7 @@ class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps ...@@ -71,7 +71,7 @@ class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps
step 'I should see label color error message' do step 'I should see label color error message' do
page.within '.label-form' do page.within '.label-form' do
expect(page).to have_content 'Color is invalid' expect(page).to have_content 'Color must be a valid color code'
end end
end end
......
...@@ -70,8 +70,6 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps ...@@ -70,8 +70,6 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps
step 'I should see hook service down error message' do step 'I should see hook service down error message' do
expect(page).to have_selector '.flash-alert', expect(page).to have_selector '.flash-alert',
text: 'Hook execution failed. '\ text: 'Hook execution failed: Exception from'
'Ensure hook URL is correct and '\
'service is up.'
end end
end end
...@@ -9,33 +9,40 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps ...@@ -9,33 +9,40 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end end
step 'I click to emoji-picker' do step 'I click to emoji-picker' do
page.within ".awards-controls" do page.within '.awards-controls' do
page.find(".add-award").click page.find('.add-award').click
end end
end end
step 'I click to emoji in the picker' do step 'I click to emoji in the picker' do
page.within ".awards-menu" do page.within '.awards-menu' do
page.first("img").click page.first('img').click
end end
end end
step 'I can remove it by clicking to icon' do step 'I can remove it by clicking to icon' do
page.within ".awards" do page.within '.awards' do
page.first(".award").click page.first('.award').click
expect(page).to_not have_selector ".award" expect(page).to_not have_selector '.award'
end end
end end
step 'I have award added' do step 'I have award added' do
page.within ".awards" do page.within '.awards' do
expect(page).to have_selector ".award" expect(page).to have_selector '.award'
expect(page.find(".award .counter")).to have_content "1" expect(page.find('.award .counter')).to have_content '1'
end end
end end
step 'project "Shop" has issue "Bugfix"' do step 'project "Shop" has issue "Bugfix"' do
@project = Project.find_by(name: "Shop") @project = Project.find_by(name: 'Shop')
@issue = create(:issue, title: "Bugfix", project: project) @issue = create(:issue, title: 'Bugfix', project: project)
end
step 'I leave comment with a single emoji' do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: ':smile:'
click_button 'Add Comment'
end
end end
end end
...@@ -55,7 +55,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps ...@@ -55,7 +55,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I should see label color error message' do step 'I should see label color error message' do
page.within '.label-form' do page.within '.label-form' do
expect(page).to have_content 'Color is invalid' expect(page).to have_content 'Color must be a valid color code'
end end
end end
......
...@@ -32,4 +32,8 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps ...@@ -32,4 +32,8 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
step 'I am signed in as a developer of the project' do step 'I am signed in as a developer of the project' do
login_as(@user) login_as(@user)
end end
step 'I should see merge request merged' do
expect(page).to have_content('The changes were merged into')
end
end end
...@@ -87,6 +87,17 @@ module SharedDiffNote ...@@ -87,6 +87,17 @@ module SharedDiffNote
end end
end end
step 'I write a diff comment like ":smile:"' do
page.within(diff_file_selector) do
click_diff_line(sample_commit.line_code)
page.within("form[rel$='#{sample_commit.line_code}']") do
fill_in 'note[note]', with: ':smile:'
click_button('Add Comment')
end
end
end
step 'I submit the diff comment' do step 'I submit the diff comment' do
page.within(diff_file_selector) do page.within(diff_file_selector) do
click_button("Add Comment") click_button("Add Comment")
...@@ -197,6 +208,12 @@ module SharedDiffNote ...@@ -197,6 +208,12 @@ module SharedDiffNote
end end
end end
step 'I should see a diff comment with an emoji image' do
page.within("#{diff_file_selector} .note") do
expect(page).to have_xpath("//img[@alt=':smile:']")
end
end
step 'I click side-by-side diff button' do step 'I click side-by-side diff button' do
find('#parallel-diff-btn').trigger('click') find('#parallel-diff-btn').trigger('click')
end end
......
...@@ -7,8 +7,12 @@ module API ...@@ -7,8 +7,12 @@ module API
helpers do helpers do
def map_public_to_visibility_level(attrs) def map_public_to_visibility_level(attrs)
publik = attrs.delete(:public) publik = attrs.delete(:public)
publik = parse_boolean(publik) if publik.present? && !attrs[:visibility_level].present?
attrs[:visibility_level] = Gitlab::VisibilityLevel::PUBLIC if !attrs[:visibility_level].present? && publik == true publik = parse_boolean(publik)
# Since setting the public attribute to private could mean either
# private or internal, use the more conservative option, private.
attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE
end
attrs attrs
end end
end end
......
module Gitlab
module Blacklist
extend self
def path
%w(
admin
dashboard
files
groups
help
profile
projects
search
public
assets
u
s
teams
merge_requests
issues
users
snippets
services
repository
hooks
notes
unsubscribes
all
ci
)
end
end
end
...@@ -18,10 +18,7 @@ module Gitlab ...@@ -18,10 +18,7 @@ module Gitlab
# homepage: String, # homepage: String,
# }, # },
# commits: Array, # commits: Array,
# total_commits_count: Fixnum, # total_commits_count: Fixnum
# added: ["CHANGELOG"],
# modified: [],
# removed: ["tmp/file.txt"]
# } # }
# #
def build(project, user, oldrev, newrev, ref, commits = [], message = nil) def build(project, user, oldrev, newrev, ref, commits = [], message = nil)
...@@ -33,11 +30,12 @@ module Gitlab ...@@ -33,11 +30,12 @@ module Gitlab
# For performance purposes maximum 20 latest commits # For performance purposes maximum 20 latest commits
# will be passed as post receive hook data. # will be passed as post receive hook data.
commit_attrs = commits_limited.map(&:hook_attrs) commit_attrs = commits_limited.map do |commit|
commit.hook_attrs(with_changed_files: true)
end
type = Gitlab::Git.tag_ref?(ref) ? "tag_push" : "push" type = Gitlab::Git.tag_ref?(ref) ? "tag_push" : "push"
repo_changes = repo_changes(project, newrev, oldrev)
# Hash to be passed as post_receive_data # Hash to be passed as post_receive_data
data = { data = {
object_kind: type, object_kind: type,
...@@ -60,10 +58,7 @@ module Gitlab ...@@ -60,10 +58,7 @@ module Gitlab
visibility_level: project.visibility_level visibility_level: project.visibility_level
}, },
commits: commit_attrs, commits: commit_attrs,
total_commits_count: commits_count, total_commits_count: commits_count
added: repo_changes[:added],
modified: repo_changes[:modified],
removed: repo_changes[:removed]
} }
data data
...@@ -94,27 +89,6 @@ module Gitlab ...@@ -94,27 +89,6 @@ module Gitlab
newrev newrev
end end
end end
def repo_changes(project, newrev, oldrev)
changes = { added: [], modified: [], removed: [] }
compare_result = CompareService.new.
execute(project, newrev, project, oldrev)
if compare_result
compare_result.diffs.each do |diff|
case true
when diff.deleted_file
changes[:removed] << diff.old_path
when diff.renamed_file, diff.new_file
changes[:added] << diff.new_path
else
changes[:modified] << diff.new_path
end
end
end
changes
end
end end
end end
end end
...@@ -56,7 +56,7 @@ server { ...@@ -56,7 +56,7 @@ server {
listen [::]:80 ipv6only=on default_server; listen [::]:80 ipv6only=on default_server;
server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com
server_tokens off; ## Don't show the nginx version number, a security best practice server_tokens off; ## Don't show the nginx version number, a security best practice
return 301 https://$server_name$request_uri; return 301 https://$http_host$request_uri;
access_log /var/log/nginx/gitlab_access.log; access_log /var/log/nginx/gitlab_access.log;
error_log /var/log/nginx/gitlab_error.log; error_log /var/log/nginx/gitlab_error.log;
} }
......
...@@ -110,6 +110,26 @@ describe Projects::CommitController do ...@@ -110,6 +110,26 @@ describe Projects::CommitController do
expect(response.body).to match(/^diff --git/) expect(response.body).to match(/^diff --git/)
end end
end end
context 'commit that removes a submodule' do
render_views
let(:fork_project) { create(:forked_project_with_submodules) }
let(:commit) { fork_project.commit('remove-submodule') }
before do
fork_project.team << [user, :master]
end
it 'renders it' do
get(:show,
namespace_id: fork_project.namespace.to_param,
project_id: fork_project.to_param,
id: commit.id)
expect(response).to be_success
end
end
end end
describe "#branches" do describe "#branches" do
......
require 'spec_helper'
describe Groups::MilestonesController do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:project2) { create(:empty_project, group: group) }
let(:user) { create(:user) }
let(:title) { '肯定不是中文的问题' }
before do
sign_in(user)
group.add_owner(user)
project.team << [user, :master]
controller.instance_variable_set(:@group, group)
end
describe "#create" do
it "should create group milestone with Chinese title" do
post :create,
group_id: group.id,
milestone: { project_ids: [project.id, project2.id], title: title }
expect(response).to redirect_to(group_milestone_path(group, title.to_slug.to_s, title: title))
expect(Milestone.where(title: title).count).to eq(2)
end
end
end
...@@ -10,6 +10,30 @@ describe Projects::MergeRequestsController do ...@@ -10,6 +10,30 @@ describe Projects::MergeRequestsController do
project.team << [user, :master] project.team << [user, :master]
end end
describe '#new' do
context 'merge request that removes a submodule' do
render_views
let(:fork_project) { create(:forked_project_with_submodules) }
before do
fork_project.team << [user, :master]
end
it 'renders it' do
get :new,
namespace_id: fork_project.namespace.to_param,
project_id: fork_project.to_param,
merge_request: {
source_branch: 'remove-submodule',
target_branch: 'master'
}
expect(response).to be_success
end
end
end
describe "#show" do describe "#show" do
shared_examples "export merge as" do |format| shared_examples "export merge as" do |format|
it "should generally work" do it "should generally work" do
......
...@@ -5,7 +5,7 @@ describe Projects::MilestonesController do ...@@ -5,7 +5,7 @@ describe Projects::MilestonesController do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:milestone) { create(:milestone, project: project) } let(:milestone) { create(:milestone, project: project) }
let(:issue) { create(:issue, project: project, milestone: milestone) } let(:issue) { create(:issue, project: project, milestone: milestone) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) } let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) }
before do before do
sign_in(user) sign_in(user)
...@@ -15,10 +15,9 @@ describe Projects::MilestonesController do ...@@ -15,10 +15,9 @@ describe Projects::MilestonesController do
describe "#destroy" do describe "#destroy" do
it "should remove milestone" do it "should remove milestone" do
merge_request.reload
expect(issue.milestone_id).to eq(milestone.id) expect(issue.milestone_id).to eq(milestone.id)
delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.id, format: :js delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid, format: :js
expect(response).to be_success expect(response).to be_success
expect(Event.first.action).to eq(Event::DESTROYED) expect(Event.first.action).to eq(Event::DESTROYED)
......
...@@ -2,6 +2,7 @@ require 'spec_helper' ...@@ -2,6 +2,7 @@ require 'spec_helper'
describe 'Comments', feature: true do describe 'Comments', feature: true do
include RepoHelpers include RepoHelpers
include WaitForAjax
describe 'On a merge request', js: true, feature: true do describe 'On a merge request', js: true, feature: true do
let!(:merge_request) { create(:merge_request) } let!(:merge_request) { create(:merge_request) }
...@@ -123,8 +124,8 @@ describe 'Comments', feature: true do ...@@ -123,8 +124,8 @@ describe 'Comments', feature: true do
it 'removes the attachment div and resets the edit form' do it 'removes the attachment div and resets the edit form' do
find('.js-note-attachment-delete').click find('.js-note-attachment-delete').click
is_expected.not_to have_css('.note-attachment') is_expected.not_to have_css('.note-attachment')
expect(find('.current-note-edit-form', visible: false)). is_expected.not_to have_css('.current-note-edit-form')
not_to be_visible wait_for_ajax
end end
end end
end end
......
...@@ -278,7 +278,7 @@ describe ApplicationHelper do ...@@ -278,7 +278,7 @@ describe ApplicationHelper do
el = element.next_element el = element.next_element
expect(el.name).to eq 'script' expect(el.name).to eq 'script'
expect(el.text).to include "$('.js-timeago').timeago()" expect(el.text).to include "$('.js-timeago').last().timeago()"
end end
it 'allows the script tag to be excluded' do it 'allows the script tag to be excluded' do
......
...@@ -17,9 +17,9 @@ describe 'Gitlab::PushDataBuilder' do ...@@ -17,9 +17,9 @@ describe 'Gitlab::PushDataBuilder' do
it { expect(data[:repository][:git_ssh_url]).to eq(project.ssh_url_to_repo) } it { expect(data[:repository][:git_ssh_url]).to eq(project.ssh_url_to_repo) }
it { expect(data[:repository][:visibility_level]).to eq(project.visibility_level) } it { expect(data[:repository][:visibility_level]).to eq(project.visibility_level) }
it { expect(data[:total_commits_count]).to eq(3) } it { expect(data[:total_commits_count]).to eq(3) }
it { expect(data[:added]).to eq(["gitlab-grack"]) } it { expect(data[:commits].first[:added]).to eq(["gitlab-grack"]) }
it { expect(data[:modified]).to eq([".gitmodules", "files/ruby/popen.rb", "files/ruby/regex.rb"]) } it { expect(data[:commits].first[:modified]).to eq([".gitmodules"]) }
it { expect(data[:removed]).to eq([]) } it { expect(data[:commits].first[:removed]).to eq([]) }
end end
describe :build do describe :build do
...@@ -38,8 +38,5 @@ describe 'Gitlab::PushDataBuilder' do ...@@ -38,8 +38,5 @@ describe 'Gitlab::PushDataBuilder' do
it { expect(data[:ref]).to eq('refs/tags/v1.1.0') } it { expect(data[:ref]).to eq('refs/tags/v1.1.0') }
it { expect(data[:commits]).to be_empty } it { expect(data[:commits]).to be_empty }
it { expect(data[:total_commits_count]).to be_zero } it { expect(data[:total_commits_count]).to be_zero }
it { expect(data[:added]).to eq([]) }
it { expect(data[:modified]).to eq([]) }
it { expect(data[:removed]).to eq([]) }
end end
end end
...@@ -36,6 +36,22 @@ describe ApplicationSetting, models: true do ...@@ -36,6 +36,22 @@ describe ApplicationSetting, models: true do
it { expect(setting).to be_valid } it { expect(setting).to be_valid }
describe 'validations' do
let(:http) { 'http://example.com' }
let(:https) { 'https://example.com' }
let(:ftp) { 'ftp://example.com' }
it { is_expected.to allow_value(nil).for(:home_page_url) }
it { is_expected.to allow_value(http).for(:home_page_url) }
it { is_expected.to allow_value(https).for(:home_page_url) }
it { is_expected.not_to allow_value(ftp).for(:home_page_url) }
it { is_expected.to allow_value(nil).for(:after_sign_out_path) }
it { is_expected.to allow_value(http).for(:after_sign_out_path) }
it { is_expected.to allow_value(https).for(:after_sign_out_path) }
it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) }
end
context 'restricted signup domains' do context 'restricted signup domains' do
it 'set single domain' do it 'set single domain' do
setting.restricted_signup_domains_raw = 'example.com' setting.restricted_signup_domains_raw = 'example.com'
......
...@@ -20,6 +20,21 @@ describe BroadcastMessage do ...@@ -20,6 +20,21 @@ describe BroadcastMessage do
it { is_expected.to be_valid } it { is_expected.to be_valid }
describe 'validations' do
let(:triplet) { '#000' }
let(:hex) { '#AABBCC' }
it { is_expected.to allow_value(nil).for(:color) }
it { is_expected.to allow_value(triplet).for(:color) }
it { is_expected.to allow_value(hex).for(:color) }
it { is_expected.not_to allow_value('000').for(:color) }
it { is_expected.to allow_value(nil).for(:font) }
it { is_expected.to allow_value(triplet).for(:font) }
it { is_expected.to allow_value(hex).for(:font) }
it { is_expected.not_to allow_value('000').for(:font) }
end
describe :current do describe :current do
it "should return last message if time match" do it "should return last message if time match" do
broadcast_message = create(:broadcast_message, starts_at: Time.now.yesterday, ends_at: Time.now.tomorrow) broadcast_message = create(:broadcast_message, starts_at: Time.now.yesterday, ends_at: Time.now.tomorrow)
......
...@@ -107,4 +107,15 @@ eos ...@@ -107,4 +107,15 @@ eos
# Include the subject in the repository stub. # Include the subject in the repository stub.
let(:extra_commits) { [subject] } let(:extra_commits) { [subject] }
end end
describe '#hook_attrs' do
let(:data) { commit.hook_attrs(with_changed_files: true) }
it { expect(data).to be_a(Hash) }
it { expect(data[:message]).to include('Add submodule from gitlab.com') }
it { expect(data[:timestamp]).to eq('2014-02-27T11:01:38+02:00') }
it { expect(data[:added]).to eq(["gitlab-grack"]) }
it { expect(data[:modified]).to eq([".gitmodules"]) }
it { expect(data[:removed]).to eq([]) }
end
end end
...@@ -71,5 +71,11 @@ describe ProjectHook do ...@@ -71,5 +71,11 @@ describe ProjectHook do
expect { @project_hook.execute(@data, 'push_hooks') }.to raise_error(RuntimeError) expect { @project_hook.execute(@data, 'push_hooks') }.to raise_error(RuntimeError)
end end
it "handles SSL exceptions" do
expect(WebHook).to receive(:post).and_raise(OpenSSL::SSL::SSLError.new('SSL error'))
expect(@project_hook.execute(@data, 'push_hooks')).to eq([false, 'SSL error'])
end
end end
end end
...@@ -4,6 +4,7 @@ describe Repository do ...@@ -4,6 +4,7 @@ describe Repository do
include RepoHelpers include RepoHelpers
let(:repository) { create(:project).repository } let(:repository) { create(:project).repository }
let(:user) { create(:user) }
describe :branch_names_contains do describe :branch_names_contains do
subject { repository.branch_names_contains(sample_commit.id) } subject { repository.branch_names_contains(sample_commit.id) }
...@@ -99,5 +100,104 @@ describe Repository do ...@@ -99,5 +100,104 @@ describe Repository do
it { expect(subject.startline).to eq(186) } it { expect(subject.startline).to eq(186) }
it { expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") } it { expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") }
end end
end end
describe :add_branch do
context 'when pre hooks were successful' do
it 'should run without errors' do
hook = double(trigger: true)
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
expect { repository.add_branch(user, 'new_feature', 'master') }.not_to raise_error
end
it 'should create the branch' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true)
branch = repository.add_branch(user, 'new_feature', 'master')
expect(branch.name).to eq('new_feature')
end
end
context 'when pre hooks failed' do
it 'should get an error' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
expect do
repository.add_branch(user, 'new_feature', 'master')
end.to raise_error(GitHooksService::PreReceiveError)
end
it 'should not create the branch' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
expect do
repository.add_branch(user, 'new_feature', 'master')
end.to raise_error(GitHooksService::PreReceiveError)
expect(repository.find_branch('new_feature')).to be_nil
end
end
end
describe :rm_branch do
context 'when pre hooks were successful' do
it 'should run without errors' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true)
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
end
it 'should delete the branch' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true)
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
expect(repository.find_branch('feature')).to be_nil
end
end
context 'when pre hooks failed' do
it 'should get an error' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
expect do
repository.rm_branch(user, 'new_feature')
end.to raise_error(GitHooksService::PreReceiveError)
end
it 'should not delete the branch' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
expect do
repository.rm_branch(user, 'feature')
end.to raise_error(GitHooksService::PreReceiveError)
expect(repository.find_branch('feature')).not_to be_nil
end
end
end
describe :commit_with_hooks do
context 'when pre hooks were successful' do
it 'should run without errors' do
expect_any_instance_of(GitHooksService).to receive(:execute).and_return(true)
expect do
repository.commit_with_hooks(user, 'feature') { sample_commit.id }
end.not_to raise_error
end
end
context 'when pre hooks failed' do
it 'should get an error' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
expect do
repository.commit_with_hooks(user, 'feature') { sample_commit.id }
end.to raise_error(GitHooksService::PreReceiveError)
end
end
end
end end
...@@ -91,7 +91,23 @@ describe User do ...@@ -91,7 +91,23 @@ describe User do
end end
describe 'validations' do describe 'validations' do
it { is_expected.to validate_presence_of(:username) } describe 'username' do
it 'validates presence' do
expect(subject).to validate_presence_of(:username)
end
it 'rejects blacklisted names' do
user = build(:user, username: 'dashboard')
expect(user).not_to be_valid
expect(user.errors.values).to eq [['dashboard is a reserved name']]
end
it 'validates uniqueness' do
expect(subject).to validate_uniqueness_of(:username)
end
end
it { is_expected.to validate_presence_of(:projects_limit) } it { is_expected.to validate_presence_of(:projects_limit) }
it { is_expected.to validate_numericality_of(:projects_limit) } it { is_expected.to validate_numericality_of(:projects_limit) }
it { is_expected.to allow_value(0).for(:projects_limit) } it { is_expected.to allow_value(0).for(:projects_limit) }
......
...@@ -47,7 +47,7 @@ describe API::API, api: true do ...@@ -47,7 +47,7 @@ describe API::API, api: true do
name: 'Foo', name: 'Foo',
color: '#FFAA' color: '#FFAA'
expect(response.status).to eq(400) expect(response.status).to eq(400)
expect(json_response['message']['color']).to eq(['is invalid']) expect(json_response['message']['color']).to eq(['must be a valid color code'])
end end
it 'should return 400 for too long color code' do it 'should return 400 for too long color code' do
...@@ -55,7 +55,7 @@ describe API::API, api: true do ...@@ -55,7 +55,7 @@ describe API::API, api: true do
name: 'Foo', name: 'Foo',
color: '#FFAAFFFF' color: '#FFAAFFFF'
expect(response.status).to eq(400) expect(response.status).to eq(400)
expect(json_response['message']['color']).to eq(['is invalid']) expect(json_response['message']['color']).to eq(['must be a valid color code'])
end end
it 'should return 400 for invalid name' do it 'should return 400 for invalid name' do
...@@ -151,12 +151,12 @@ describe API::API, api: true do ...@@ -151,12 +151,12 @@ describe API::API, api: true do
expect(json_response['message']['title']).to eq(['is invalid']) expect(json_response['message']['title']).to eq(['is invalid'])
end end
it 'should return 400 for invalid name' do it 'should return 400 when color code is too short' do
put api("/projects/#{project.id}/labels", user), put api("/projects/#{project.id}/labels", user),
name: 'label1', name: 'label1',
color: '#FF' color: '#FF'
expect(response.status).to eq(400) expect(response.status).to eq(400)
expect(json_response['message']['color']).to eq(['is invalid']) expect(json_response['message']['color']).to eq(['must be a valid color code'])
end end
it 'should return 400 for too long color code' do it 'should return 400 for too long color code' do
...@@ -164,7 +164,7 @@ describe API::API, api: true do ...@@ -164,7 +164,7 @@ describe API::API, api: true do
name: 'Foo', name: 'Foo',
color: '#FFAAFFFF' color: '#FFAAFFFF'
expect(response.status).to eq(400) expect(response.status).to eq(400)
expect(json_response['message']['color']).to eq(['is invalid']) expect(json_response['message']['color']).to eq(['must be a valid color code'])
end end
end end
end end
...@@ -742,6 +742,18 @@ describe API::API, api: true do ...@@ -742,6 +742,18 @@ describe API::API, api: true do
end end
end end
it 'should update visibility_level from public to private' do
project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
project_param = { public: false }
put api("/projects/#{project3.id}", user), project_param
expect(response.status).to eq(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
it 'should not update name to existing name' do it 'should not update name to existing name' do
project_param = { name: project3.name } project_param = { name: project3.name }
put api("/projects/#{project.id}", user), project_param put api("/projects/#{project.id}", user), project_param
......
...@@ -153,7 +153,7 @@ describe API::API, api: true do ...@@ -153,7 +153,7 @@ describe API::API, api: true do
expect(json_response['message']['projects_limit']). expect(json_response['message']['projects_limit']).
to eq(['must be greater than or equal to 0']) to eq(['must be greater than or equal to 0'])
expect(json_response['message']['username']). expect(json_response['message']['username']).
to eq([Gitlab::Regex.send(:namespace_regex_message)]) to eq([Gitlab::Regex.namespace_regex_message])
end end
it "shouldn't available for non admin users" do it "shouldn't available for non admin users" do
...@@ -296,7 +296,7 @@ describe API::API, api: true do ...@@ -296,7 +296,7 @@ describe API::API, api: true do
expect(json_response['message']['projects_limit']). expect(json_response['message']['projects_limit']).
to eq(['must be greater than or equal to 0']) to eq(['must be greater than or equal to 0'])
expect(json_response['message']['username']). expect(json_response['message']['username']).
to eq([Gitlab::Regex.send(:namespace_regex_message)]) to eq([Gitlab::Regex.namespace_regex_message])
end end
context "with existing user" do context "with existing user" do
......
require 'spec_helper'
describe GitHooksService do
include RepoHelpers
let(:user) { create :user }
let(:project) { create :project }
let(:service) { GitHooksService.new }
before do
@blankrev = Gitlab::Git::BLANK_SHA
@oldrev = sample_commit.parent_id
@newrev = sample_commit.id
@ref = 'refs/heads/feature'
@repo_path = project.repository.path_to_repo
end
describe '#execute' do
context 'when receive hooks were successful' do
it 'should call post-receive hook' do
hook = double(trigger: true)
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
expect(service.execute(user, @repo_path, @blankrev, @newrev, @ref) { }).to eq(true)
end
end
context 'when pre-receive hook failed' do
it 'should not call post-receive hook' do
expect(service).to receive(:run_hook).with('pre-receive').and_return(false)
expect(service).not_to receive(:run_hook).with('post-receive')
expect do
service.execute(user, @repo_path, @blankrev, @newrev, @ref)
end.to raise_error(GitHooksService::PreReceiveError)
end
end
context 'when update hook failed' do
it 'should not call post-receive hook' do
expect(service).to receive(:run_hook).with('pre-receive').and_return(true)
expect(service).to receive(:run_hook).with('update').and_return(false)
expect(service).not_to receive(:run_hook).with('post-receive')
expect do
service.execute(user, @repo_path, @blankrev, @newrev, @ref)
end.to raise_error(GitHooksService::PreReceiveError)
end
end
end
end
...@@ -21,7 +21,8 @@ module TestEnv ...@@ -21,7 +21,8 @@ module TestEnv
# We currently only need a subset of the branches # We currently only need a subset of the branches
FORKED_BRANCH_SHA = { FORKED_BRANCH_SHA = {
'add-submodule-version-bump' => '3f547c08', 'add-submodule-version-bump' => '3f547c08',
'master' => '5937ac0' 'master' => '5937ac0',
'remove-submodule' => '2a33e0c0'
} }
# Test environment # Test environment
......
module WaitForAjax
def wait_for_ajax
Timeout.timeout(Capybara.default_wait_time) do
loop until finished_all_ajax_requests?
end
end
def finished_all_ajax_requests?
page.evaluate_script('jQuery.active').zero?
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment