Commit d7c647f5 authored by Yorick Peterse's avatar Yorick Peterse

Merge branch 'master' into 8-7-stable

parents f024fd2b 14491285
...@@ -2,7 +2,6 @@ image: "ruby:2.1" ...@@ -2,7 +2,6 @@ image: "ruby:2.1"
services: services:
- mysql:latest - mysql:latest
- postgres:latest
- redis:latest - redis:latest
cache: cache:
...@@ -35,134 +34,86 @@ spec:feature: ...@@ -35,134 +34,86 @@ spec:feature:
script: script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
tags:
- ruby
- mysql
spec:api: spec:api:
stage: test stage: test
script: script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api
tags:
- ruby
- mysql
spec:models: spec:models:
stage: test stage: test
script: script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models
tags:
- ruby
- mysql
spec:lib: spec:lib:
stage: test stage: test
script: script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib
tags:
- ruby
- mysql
spec:services: spec:services:
stage: test stage: test
script: script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services
tags:
- ruby
- mysql
spec:other: spec:other:
stage: test stage: test
script: script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
tags:
- ruby
- mysql
spinach:project:half: spinach:project:half:
stage: test stage: test
script: script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half
tags:
- ruby
- mysql
spinach:project:rest: spinach:project:rest:
stage: test stage: test
script: script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest
tags:
- ruby
- mysql
spinach:other: spinach:other:
stage: test stage: test
script: script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other
tags:
- ruby
- mysql
teaspoon: teaspoon:
stage: test stage: test
script: script:
- RAILS_ENV=test bundle exec teaspoon - RAILS_ENV=test bundle exec teaspoon
tags:
- ruby
- mysql
rubocop: rubocop:
stage: test stage: test
script: script:
- bundle exec rubocop - bundle exec rubocop
tags:
- ruby
- mysql
scss-lint: scss-lint:
stage: test stage: test
script: script:
- bundle exec rake scss_lint - bundle exec rake scss_lint
tags:
- ruby
brakeman: brakeman:
stage: test stage: test
script: script:
- bundle exec rake brakeman - bundle exec rake brakeman
tags:
- ruby
- mysql
flog: flog:
stage: test stage: test
script: script:
- bundle exec rake flog - bundle exec rake flog
tags:
- ruby
- mysql
flay: flay:
stage: test stage: test
script: script:
- bundle exec rake flay - bundle exec rake flay
tags:
- ruby
- mysql
bundler:audit: bundler:audit:
stage: test stage: test
only: only:
- master - master
script: script:
- "bundle exec bundle-audit update" - "bundle exec bundle-audit check --update --ignore OSVDB-115941"
- "bundle exec bundle-audit check --ignore OSVDB-115941"
tags:
- ruby
- mysql
# Ruby 2.2 jobs # Ruby 2.2 jobs
...@@ -178,9 +129,6 @@ spec:feature:ruby22: ...@@ -178,9 +129,6 @@ spec:feature:ruby22:
key: "ruby22" key: "ruby22"
paths: paths:
- vendor - vendor
tags:
- ruby
- mysql
spec:api:ruby22: spec:api:ruby22:
stage: test stage: test
...@@ -193,9 +141,6 @@ spec:api:ruby22: ...@@ -193,9 +141,6 @@ spec:api:ruby22:
key: "ruby22" key: "ruby22"
paths: paths:
- vendor - vendor
tags:
- ruby
- mysql
spec:models:ruby22: spec:models:ruby22:
stage: test stage: test
...@@ -208,9 +153,6 @@ spec:models:ruby22: ...@@ -208,9 +153,6 @@ spec:models:ruby22:
key: "ruby22" key: "ruby22"
paths: paths:
- vendor - vendor
tags:
- ruby
- mysql
spec:lib:ruby22: spec:lib:ruby22:
stage: test stage: test
...@@ -223,9 +165,6 @@ spec:lib:ruby22: ...@@ -223,9 +165,6 @@ spec:lib:ruby22:
key: "ruby22" key: "ruby22"
paths: paths:
- vendor - vendor
tags:
- ruby
- mysql
spec:services:ruby22: spec:services:ruby22:
stage: test stage: test
...@@ -238,9 +177,6 @@ spec:services:ruby22: ...@@ -238,9 +177,6 @@ spec:services:ruby22:
key: "ruby22" key: "ruby22"
paths: paths:
- vendor - vendor
tags:
- ruby
- mysql
spec:other:ruby22: spec:other:ruby22:
stage: test stage: test
...@@ -253,9 +189,6 @@ spec:other:ruby22: ...@@ -253,9 +189,6 @@ spec:other:ruby22:
key: "ruby22" key: "ruby22"
paths: paths:
- vendor - vendor
tags:
- ruby
- mysql
spinach:project:half:ruby22: spinach:project:half:ruby22:
stage: test stage: test
...@@ -269,9 +202,6 @@ spinach:project:half:ruby22: ...@@ -269,9 +202,6 @@ spinach:project:half:ruby22:
key: "ruby22" key: "ruby22"
paths: paths:
- vendor - vendor
tags:
- ruby
- mysql
spinach:project:rest:ruby22: spinach:project:rest:ruby22:
stage: test stage: test
...@@ -285,9 +215,6 @@ spinach:project:rest:ruby22: ...@@ -285,9 +215,6 @@ spinach:project:rest:ruby22:
key: "ruby22" key: "ruby22"
paths: paths:
- vendor - vendor
tags:
- ruby
- mysql
spinach:other:ruby22: spinach:other:ruby22:
stage: test stage: test
...@@ -301,10 +228,6 @@ spinach:other:ruby22: ...@@ -301,10 +228,6 @@ spinach:other:ruby22:
key: "ruby22" key: "ruby22"
paths: paths:
- vendor - vendor
tags:
- ruby
- mysql
notify:slack: notify:slack:
stage: notifications stage: notifications
......
...@@ -2,17 +2,23 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -2,17 +2,23 @@ Please view this file on the master branch, on stable branches it's out of date.
v 8.7.0 (unreleased) v 8.7.0 (unreleased)
- All images in discussions and wikis now link to their source files !3464 (Connor Shea). - All images in discussions and wikis now link to their source files !3464 (Connor Shea).
- Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu)
- Improved Markdown rendering performance !3389 (Yorick Peterse) - Improved Markdown rendering performance !3389 (Yorick Peterse)
- Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu) - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu)
- Expose project badges in project settings
- Preserve time notes/comments have been updated at when moving issue - Preserve time notes/comments have been updated at when moving issue
- Make HTTP(s) label consistent on clone bar (Stan Hu) - Make HTTP(s) label consistent on clone bar (Stan Hu)
- Expose label description in API (Mariusz Jachimowicz) - Expose label description in API (Mariusz Jachimowicz)
- Allow back dating on issues when created through the API - Allow back dating on issues when created through the API
- Fix Error 500 after renaming a project path (Stan Hu)
- Fix avatar stretching by providing a cropping feature - Fix avatar stretching by providing a cropping feature
- Allow SAML to handle external users based on user's information !3530
- Add endpoints to archive or unarchive a project !3372 - Add endpoints to archive or unarchive a project !3372
- Add links to CI setup documentation from project settings and builds pages - Add links to CI setup documentation from project settings and builds pages
- Handle nil descriptions in Slack issue messages (Stan Hu) - Handle nil descriptions in Slack issue messages (Stan Hu)
- Add default scope to projects to exclude projects pending deletion - Add default scope to projects to exclude projects pending deletion
- Ensure empty recipients are rejected in BuildsEmailService
- API: Ability to filter milestones by state `active` and `closed` (Robert Schilling)
- Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.) - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
- Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.) - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
- Gracefully handle notes on deleted commits in merge requests (Stan Hu) - Gracefully handle notes on deleted commits in merge requests (Stan Hu)
...@@ -20,7 +26,13 @@ v 8.7.0 (unreleased) ...@@ -20,7 +26,13 @@ v 8.7.0 (unreleased)
- Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla) - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla)
- Remove "Congratulations!" tweet button on newly-created project. (Connor Shea) - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea)
- Improved UX of the navigation sidebar - Improved UX of the navigation sidebar
- Fix admin/projects when using visibility levels on search (PotHix)
- Build status notifications - Build status notifications
- API: Expose user location (Robert Schilling)
v 8.6.5 (unreleased)
- Only update repository language if it is not set to improve performance
- Check permissions when user attempts to import members from another project
v 8.6.4 v 8.6.4
- Don't attempt to fetch any tags from a forked repo (Stan Hu) - Don't attempt to fetch any tags from a forked repo (Stan Hu)
...@@ -140,6 +152,9 @@ v 8.6.0 ...@@ -140,6 +152,9 @@ v 8.6.0
- Trigger a todo for mentions on commits page - Trigger a todo for mentions on commits page
- Let project owners and admins soft delete issues and merge requests - Let project owners and admins soft delete issues and merge requests
v 8.5.9
- Don't attempt to fetch any tags from a forked repo (Stan Hu).
v 8.5.8 v 8.5.8
- Bump Git version requirement to 2.7.4 - Bump Git version requirement to 2.7.4
...@@ -281,6 +296,12 @@ v 8.5.0 ...@@ -281,6 +296,12 @@ v 8.5.0
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul) - Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
- Add Todos - Add Todos
v 8.4.7
- Don't attempt to fetch any tags from a forked repo (Stan Hu).
v 8.4.6
- Bump Git version requirement to 2.7.4
v 8.4.5 v 8.4.5
- No CE-specific changes - No CE-specific changes
...@@ -394,6 +415,12 @@ v 8.4.0 ...@@ -394,6 +415,12 @@ v 8.4.0
- Add IP check against DNSBLs at account sign-up - Add IP check against DNSBLs at account sign-up
- Added cache:key to .gitlab-ci.yml allowing to fine tune the caching - Added cache:key to .gitlab-ci.yml allowing to fine tune the caching
v 8.3.6
- Don't attempt to fetch any tags from a forked repo (Stan Hu).
v 8.3.5
- Bump Git version requirement to 2.7.4
v 8.3.4 v 8.3.4
- Use gitlab-workhorse 0.5.4 (fixes API routing bug) - Use gitlab-workhorse 0.5.4 (fixes API routing bug)
......
...@@ -290,7 +290,7 @@ group :development, :test do ...@@ -290,7 +290,7 @@ group :development, :test do
gem 'rubocop', '~> 0.38.0', require: false gem 'rubocop', '~> 0.38.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false gem 'scss_lint', '~> 0.47.0', require: false
gem 'coveralls', '~> 0.8.2', require: false gem 'coveralls', '~> 0.8.2', require: false
gem 'simplecov', '~> 0.10.0', require: false gem 'simplecov', '~> 0.11.0', require: false
gem 'flog', require: false gem 'flog', require: false
gem 'flay', require: false gem 'flay', require: false
gem 'bundler-audit', require: false gem 'bundler-audit', require: false
......
...@@ -99,7 +99,7 @@ GEM ...@@ -99,7 +99,7 @@ GEM
bullet (5.0.0) bullet (5.0.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.9.0) uniform_notifier (~> 1.9.0)
bundler-audit (0.4.0) bundler-audit (0.5.0)
bundler (~> 1.2) bundler (~> 1.2)
thor (~> 0.18) thor (~> 0.18)
byebug (8.2.1) byebug (8.2.1)
...@@ -136,10 +136,9 @@ GEM ...@@ -136,10 +136,9 @@ GEM
colorize (0.7.7) colorize (0.7.7)
concurrent-ruby (1.0.0) concurrent-ruby (1.0.0)
connection_pool (2.2.0) connection_pool (2.2.0)
coveralls (0.8.9) coveralls (0.8.13)
json (~> 1.8) json (~> 1.8)
rest-client (>= 1.6.8, < 2) simplecov (~> 0.11.0)
simplecov (~> 0.10.0)
term-ansicolor (~> 1.3) term-ansicolor (~> 1.3)
thor (~> 0.19.1) thor (~> 0.19.1)
tins (~> 1.6.0) tins (~> 1.6.0)
...@@ -176,8 +175,6 @@ GEM ...@@ -176,8 +175,6 @@ GEM
diff-lcs (1.2.5) diff-lcs (1.2.5)
diffy (3.0.7) diffy (3.0.7)
docile (1.1.5) docile (1.1.5)
domain_name (0.5.25)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (2.2.2) doorkeeper (2.2.2)
railties (>= 3.2) railties (>= 3.2)
dropzonejs-rails (0.7.2) dropzonejs-rails (0.7.2)
...@@ -421,8 +418,6 @@ GEM ...@@ -421,8 +418,6 @@ GEM
nokogiri (~> 1.6.0) nokogiri (~> 1.6.0)
ruby_parser (~> 3.5) ruby_parser (~> 3.5)
htmlentities (4.3.4) htmlentities (4.3.4)
http-cookie (1.0.2)
domain_name (~> 0.5)
http_parser.rb (0.5.3) http_parser.rb (0.5.3)
httparty (0.13.7) httparty (0.13.7)
json (~> 1.8) json (~> 1.8)
...@@ -480,7 +475,6 @@ GEM ...@@ -480,7 +475,6 @@ GEM
nested_form (0.3.2) nested_form (0.3.2)
net-ldap (0.12.1) net-ldap (0.12.1)
net-ssh (3.0.1) net-ssh (3.0.1)
netrc (0.11.0)
newrelic_rpm (3.14.1.311) newrelic_rpm (3.14.1.311)
nokogiri (1.6.7.2) nokogiri (1.6.7.2)
mini_portile2 (~> 2.0.0.rc2) mini_portile2 (~> 2.0.0.rc2)
...@@ -657,10 +651,6 @@ GEM ...@@ -657,10 +651,6 @@ GEM
listen (~> 3.0) listen (~> 3.0)
responders (2.1.1) responders (2.1.1)
railties (>= 4.2.0, < 5.1) railties (>= 4.2.0, < 5.1)
rest-client (1.8.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 3.0)
netrc (~> 0.7)
rinku (1.7.3) rinku (1.7.3)
rotp (2.1.1) rotp (2.1.1)
rouge (1.10.1) rouge (1.10.1)
...@@ -754,7 +744,7 @@ GEM ...@@ -754,7 +744,7 @@ GEM
rufus-scheduler (>= 2.0.24) rufus-scheduler (>= 2.0.24)
sidekiq (>= 4.0.0) sidekiq (>= 4.0.0)
simple_oauth (0.1.9) simple_oauth (0.1.9)
simplecov (0.10.0) simplecov (0.11.2)
docile (~> 1.1.0) docile (~> 1.1.0)
json (~> 1.8) json (~> 1.8)
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
...@@ -845,7 +835,7 @@ GEM ...@@ -845,7 +835,7 @@ GEM
underscore-rails (1.8.3) underscore-rails (1.8.3)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.1) unf_ext (0.0.7.2)
unicode-display_width (1.0.2) unicode-display_width (1.0.2)
unicorn (4.9.0) unicorn (4.9.0)
kgio (~> 2.6) kgio (~> 2.6)
...@@ -1032,7 +1022,7 @@ DEPENDENCIES ...@@ -1032,7 +1022,7 @@ DEPENDENCIES
shoulda-matchers (~> 2.8.0) shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.0) sidekiq (~> 4.0)
sidekiq-cron (~> 0.4.0) sidekiq-cron (~> 0.4.0)
simplecov (~> 0.10.0) simplecov (~> 0.11.0)
sinatra (~> 1.4.4) sinatra (~> 1.4.4)
six (~> 0.2.0) six (~> 0.2.0)
slack-notifier (~> 1.2.0) slack-notifier (~> 1.2.0)
......
class GitLabDropdownFilter class GitLabDropdownFilter
BLUR_KEYCODES = [27, 40] BLUR_KEYCODES = [27, 40]
ARROW_KEY_CODES = [38, 40]
HAS_VALUE_CLASS = "has-value" HAS_VALUE_CLASS = "has-value"
constructor: (@input, @options) -> constructor: (@input, @options) ->
...@@ -22,19 +23,23 @@ class GitLabDropdownFilter ...@@ -22,19 +23,23 @@ class GitLabDropdownFilter
# Key events # Key events
timeout = "" timeout = ""
@input.on "keyup", (e) => @input.on "keyup", (e) =>
keyCode = e.which
return if ARROW_KEY_CODES.indexOf(keyCode) >= 0
if @input.val() isnt "" and !$inputContainer.hasClass HAS_VALUE_CLASS if @input.val() isnt "" and !$inputContainer.hasClass HAS_VALUE_CLASS
$inputContainer.addClass HAS_VALUE_CLASS $inputContainer.addClass HAS_VALUE_CLASS
else if @input.val() is "" and $inputContainer.hasClass HAS_VALUE_CLASS else if @input.val() is "" and $inputContainer.hasClass HAS_VALUE_CLASS
$inputContainer.removeClass HAS_VALUE_CLASS $inputContainer.removeClass HAS_VALUE_CLASS
if e.keyCode is 13 and @input.val() isnt "" if keyCode is 13 and @input.val() isnt ""
if @options.enterCallback if @options.enterCallback
@options.enterCallback() @options.enterCallback()
return return
clearTimeout timeout clearTimeout timeout
timeout = setTimeout => timeout = setTimeout =>
blur_field = @shouldBlur e.keyCode blur_field = @shouldBlur keyCode
search_text = @input.val() search_text = @input.val()
if blur_field and @filterInputBlur if blur_field and @filterInputBlur
...@@ -96,6 +101,7 @@ class GitLabDropdown ...@@ -96,6 +101,7 @@ class GitLabDropdown
LOADING_CLASS = "is-loading" LOADING_CLASS = "is-loading"
PAGE_TWO_CLASS = "is-page-two" PAGE_TWO_CLASS = "is-page-two"
ACTIVE_CLASS = "is-active" ACTIVE_CLASS = "is-active"
currentIndex = -1
FILTER_INPUT = '.dropdown-input .dropdown-input-field' FILTER_INPUT = '.dropdown-input .dropdown-input-field'
...@@ -145,11 +151,11 @@ class GitLabDropdown ...@@ -145,11 +151,11 @@ class GitLabDropdown
data: => data: =>
return @fullData return @fullData
callback: (data) => callback: (data) =>
currentIndex = -1
@parseData data @parseData data
@highlightRow 1
enterCallback: => enterCallback: =>
if @enterCallback if @enterCallback
@selectFirstRow() @selectRowAtIndex 0
# Event listeners # Event listeners
...@@ -171,10 +177,11 @@ class GitLabDropdown ...@@ -171,10 +177,11 @@ class GitLabDropdown
selector = ".dropdown-page-one .dropdown-content a" selector = ".dropdown-page-one .dropdown-content a"
@dropdown.on "click", selector, (e) -> @dropdown.on "click", selector, (e) ->
selected = self.rowClicked $(@) $el = $(@)
selected = self.rowClicked $el
if self.options.clicked if self.options.clicked
self.options.clicked(selected) self.options.clicked(selected, $el, e)
# Finds an element inside wrapper element # Finds an element inside wrapper element
getElement: (selector) -> getElement: (selector) ->
...@@ -218,6 +225,8 @@ class GitLabDropdown ...@@ -218,6 +225,8 @@ class GitLabDropdown
return true return true
opened: => opened: =>
@addArrowKeyEvent()
contentHtml = $('.dropdown-content', @dropdown).html() contentHtml = $('.dropdown-content', @dropdown).html()
if @remote && contentHtml is "" if @remote && contentHtml is ""
@remote.execute() @remote.execute()
...@@ -228,6 +237,7 @@ class GitLabDropdown ...@@ -228,6 +237,7 @@ class GitLabDropdown
@dropdown.trigger('shown.gl.dropdown') @dropdown.trigger('shown.gl.dropdown')
hidden: (e) => hidden: (e) =>
@removeArrayKeyEvent()
if @options.filterable if @options.filterable
@dropdown @dropdown
.find(".dropdown-input-field") .find(".dropdown-input-field")
...@@ -307,11 +317,11 @@ class GitLabDropdown ...@@ -307,11 +317,11 @@ class GitLabDropdown
if @highlight if @highlight
text = @highlightTextMatches(text, @filterInput.val()) text = @highlightTextMatches(text, @filterInput.val())
html = "<li>" html = "<li>
html += "<a href='#{url}' class='#{cssClass}'>" <a href='#{url}' class='#{cssClass}'>
html += text #{text}
html += "</a>" </a>
html += "</li>" </li>"
return html return html
...@@ -322,11 +332,11 @@ class GitLabDropdown ...@@ -322,11 +332,11 @@ class GitLabDropdown
).join('') ).join('')
noResults: -> noResults: ->
html = "<li>" html = "<li class='dropdown-menu-empty-link'>
html += "<a class='dropdown-menu-empty-link is-focused'>" <a href='#' class='is-focused'>
html += "No matching results." No matching results.
html += "</a>" </a>
html += "</li>" </li>"
highlightRow: (index) -> highlightRow: (index) ->
if @filterInput.val() isnt "" if @filterInput.val() isnt ""
...@@ -351,6 +361,8 @@ class GitLabDropdown ...@@ -351,6 +361,8 @@ class GitLabDropdown
# Toggle the dropdown label # Toggle the dropdown label
if @options.toggleLabel if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel $(@el).find(".dropdown-toggle-text").text @options.toggleLabel
else
selectedObject
else else
if !value? if !value?
field.remove() field.remove()
...@@ -366,7 +378,7 @@ class GitLabDropdown ...@@ -366,7 +378,7 @@ class GitLabDropdown
if @options.toggleLabel if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject) $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject)
if value? if value?
if !field.length if !field.length and fieldName
# Create hidden input for form # Create hidden input for form
input = "<input type='hidden' name='#{fieldName}' value='#{value}' />" input = "<input type='hidden' name='#{fieldName}' value='#{value}' />"
if @options.inputId? if @options.inputId?
...@@ -378,16 +390,81 @@ class GitLabDropdown ...@@ -378,16 +390,81 @@ class GitLabDropdown
return selectedObject return selectedObject
selectFirstRow: -> selectRowAtIndex: (index) ->
selector = '.dropdown-content li:first-child a' selector = ".dropdown-content li:not(.divider):eq(#{index}) a"
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content li:first-child a" selector = ".dropdown-page-one #{selector}"
# simulate a click on the first link # simulate a click on the first link
$(selector).trigger "click" $(selector, @dropdown).trigger "click"
addArrowKeyEvent: ->
ARROW_KEY_CODES = [38, 40]
$input = @dropdown.find(".dropdown-input-field")
selector = '.dropdown-content li:not(.divider)'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one #{selector}"
$('body').on 'keydown', (e) =>
currentKeyCode = e.which
if ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0
e.preventDefault()
e.stopImmediatePropagation()
PREV_INDEX = currentIndex
$listItems = $(selector, @dropdown)
# if @options.filterable
# $input.blur()
if currentKeyCode is 40
# Move down
currentIndex += 1 if currentIndex < ($listItems.length - 1)
else if currentKeyCode is 38
# Move up
currentIndex -= 1 if currentIndex > 0
@highlightRowAtIndex($listItems, currentIndex) if currentIndex isnt PREV_INDEX
return false
if currentKeyCode is 13
@selectRowAtIndex currentIndex
removeArrayKeyEvent: ->
$('body').off 'keydown'
highlightRowAtIndex: ($listItems, index) ->
# Remove the class for the previously focused row
$('.is-focused', @dropdown).removeClass 'is-focused'
# Update the class for the row at the specific index
$listItem = $listItems.eq(index)
$listItem.find('a:first-child').addClass "is-focused"
# Dropdown content scroll area
$dropdownContent = $listItem.closest('.dropdown-content')
dropdownScrollTop = $dropdownContent.scrollTop()
dropdownContentHeight = $dropdownContent.outerHeight()
dropdownContentTop = $dropdownContent.prop('offsetTop')
dropdownContentBottom = dropdownContentTop + dropdownContentHeight
# Get the offset bottom of the list item
listItemHeight = $listItem.outerHeight()
listItemTop = $listItem.prop('offsetTop')
listItemBottom = listItemTop + listItemHeight
if listItemBottom > dropdownContentBottom + dropdownScrollTop
# Scroll the dropdown content down
$dropdownContent.scrollTop(listItemBottom - dropdownContentBottom)
else if listItemTop < dropdownContentTop + dropdownScrollTop
# Scroll the dropdown content up
$dropdownContent.scrollTop(listItemTop - dropdownContentTop)
$.fn.glDropdown = (opts) -> $.fn.glDropdown = (opts) ->
return @.each -> return @.each ->
if (!$.data @, 'glDropdown') if (!$.data @, 'glDropdown')
$.data(@, 'glDropdown', new GitLabDropdown @, opts) $.data(@, 'glDropdown', new GitLabDropdown @, opts)
...@@ -6,25 +6,10 @@ class @Issue ...@@ -6,25 +6,10 @@ class @Issue
constructor: -> constructor: ->
# Prevent duplicate event bindings # Prevent duplicate event bindings
@disableTaskList() @disableTaskList()
@fixAffixScroll()
if $('a.btn-close').length if $('a.btn-close').length
@initTaskList() @initTaskList()
@initIssueBtnEventListeners() @initIssueBtnEventListeners()
fixAffixScroll: ->
fixAffix = ->
$discussion = $('.issuable-discussion')
$sidebar = $('.issuable-sidebar')
if $sidebar.hasClass('no-affix')
$sidebar.removeClass(['affix-top','affix'])
discussionHeight = $discussion.height()
sidebarHeight = $sidebar.height()
if sidebarHeight > discussionHeight
$discussion.height(sidebarHeight + 50)
$sidebar.addClass('no-affix')
$(window).on('resize', fixAffix)
fixAffix()
initTaskList: -> initTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('enable') $('.detail-page-description .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
...@@ -49,7 +34,7 @@ class @Issue ...@@ -49,7 +34,7 @@ class @Issue
issueStatus = if isClose then 'close' else 'open' issueStatus = if isClose then 'close' else 'open'
new Flash(issueFailMessage, 'alert') new Flash(issueFailMessage, 'alert')
success: (data, textStatus, jqXHR) -> success: (data, textStatus, jqXHR) ->
if data.saved if 'id' of data
$(document).trigger('issuable:change'); $(document).trigger('issuable:change');
if isClose if isClose
$('a.btn-close').addClass('hidden') $('a.btn-close').addClass('hidden')
......
...@@ -26,6 +26,20 @@ ...@@ -26,6 +26,20 @@
$(".selected_issue").bind "change", Issues.checkChanged $(".selected_issue").bind "change", Issues.checkChanged
# Update state filters if present in page
updateStateFilters: ->
stateFilters = $('.issues-state-filters')
newParams = {}
paramKeys = ['author_id', 'label_name', 'milestone_title', 'assignee_id', 'issue_search']
for paramKey in paramKeys
newParams[paramKey] = gl.utils.getUrlParameter(paramKey) or ''
if stateFilters.length
stateFilters.find('a').each ->
initialUrl = $(this).attr 'href'
$(this).attr 'href', gl.utils.mergeUrlParams(newParams, initialUrl)
# Make sure we trigger ajax request only after user stop typing # Make sure we trigger ajax request only after user stop typing
initSearch: -> initSearch: ->
@timer = null @timer = null
...@@ -54,6 +68,7 @@ ...@@ -54,6 +68,7 @@
# Change url so if user reload a page - search results are saved # Change url so if user reload a page - search results are saved
history.replaceState {page: issuesUrl}, document.title, issuesUrl history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issues.reload() Issues.reload()
Issues.updateStateFilters()
dataType: "json" dataType: "json"
checkChanged: -> checkChanged: ->
......
((w) ->
w.gl ?= {}
w.gl.utils ?= {}
w.gl.utils.getUrlParameter = (sParam) ->
sPageURL = decodeURIComponent(window.location.search.substring(1))
sURLVariables = sPageURL.split('&')
sParameterName = undefined
i = 0
while i < sURLVariables.length
sParameterName = sURLVariables[i].split('=')
if sParameterName[0] is sParam
return if sParameterName[1] is undefined then true else sParameterName[1]
i++
# #
# @param {Object} params - url keys and value to merge
# @param {String} url
# #
w.gl.utils.mergeUrlParams = (params, url) ->
newUrl = decodeURIComponent(url)
for paramName, paramValue of params
pattern = new RegExp "\\b(#{paramName}=).*?(&|$)"
if url.search(pattern) >= 0
newUrl = newUrl.replace pattern, "$1#{paramValue}$2"
else
newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}"
newUrl
) window
...@@ -15,8 +15,6 @@ class @MergeRequest ...@@ -15,8 +15,6 @@ class @MergeRequest
this.$('.show-all-commits').on 'click', => this.$('.show-all-commits').on 'click', =>
this.showAllCommits() this.showAllCommits()
@fixAffixScroll();
@initTabs() @initTabs()
# Prevent duplicate event bindings # Prevent duplicate event bindings
...@@ -30,20 +28,6 @@ class @MergeRequest ...@@ -30,20 +28,6 @@ class @MergeRequest
$: (selector) -> $: (selector) ->
this.$el.find(selector) this.$el.find(selector)
fixAffixScroll: ->
fixAffix = ->
$discussion = $('.issuable-discussion')
$sidebar = $('.issuable-sidebar')
if $sidebar.hasClass('no-affix')
$sidebar.removeClass(['affix-top','affix'])
discussionHeight = $discussion.height()
sidebarHeight = $sidebar.height()
if sidebarHeight > discussionHeight
$discussion.height(sidebarHeight + 50)
$sidebar.addClass('no-affix')
$(window).on('resize', fixAffix)
fixAffix()
initTabs: -> initTabs: ->
if @opts.action != 'new' if @opts.action != 'new'
# `MergeRequests#new` has no tab-persisting or lazy-loading behavior # `MergeRequests#new` has no tab-persisting or lazy-loading behavior
......
...@@ -15,6 +15,8 @@ class @MergeRequestWidget ...@@ -15,6 +15,8 @@ class @MergeRequestWidget
@pollCIStatus() @pollCIStatus()
notifyPermissions() notifyPermissions()
setOpts: (@opts) ->
mergeInProgress: (deleteSourceBranch = false)-> mergeInProgress: (deleteSourceBranch = false)->
$.ajax $.ajax
type: 'GET' type: 'GET'
...@@ -48,7 +50,7 @@ class @MergeRequestWidget ...@@ -48,7 +50,7 @@ class @MergeRequestWidget
@getCIStatus(true) @getCIStatus(true)
@readyForCICheck = false @readyForCICheck = false
), 5000 ), 10000
getCIStatus: (showNotification) -> getCIStatus: (showNotification) ->
_this = @ _this = @
...@@ -61,6 +63,10 @@ class @MergeRequestWidget ...@@ -61,6 +63,10 @@ class @MergeRequestWidget
@firstCICheck = false @firstCICheck = false
@opts.ci_status = data.status @opts.ci_status = data.status
if @opts.ci_status is ''
@opts.ci_status = data.status
return
if data.status isnt @opts.ci_status if data.status isnt @opts.ci_status
@showCIStatus data.status @showCIStatus data.status
if data.coverage if data.coverage
......
...@@ -251,13 +251,11 @@ class @Notes ...@@ -251,13 +251,11 @@ class @Notes
Sets some hidden fields in the form. Sets some hidden fields in the form.
### ###
setupMainTargetNoteForm: -> setupMainTargetNoteForm: ->
# find the form # find the form
form = $(".js-new-note-form") form = $(".js-new-note-form")
# insert the form after the button # Set a global clone of the form for later cloning
form.clone().replaceAll $(".js-main-target-form") @formClone = form.clone()
form = form.prev("form")
# show the form # show the form
@setupNoteForm(form) @setupNoteForm(form)
...@@ -266,9 +264,7 @@ class @Notes ...@@ -266,9 +264,7 @@ class @Notes
form.removeClass "js-new-note-form" form.removeClass "js-new-note-form"
form.addClass "js-main-target-form" form.addClass "js-main-target-form"
# remove unnecessary fields and buttons
form.find("#note_line_code").remove() form.find("#note_line_code").remove()
form.find(".js-close-discussion-note-form").remove()
### ###
General note form setup. General note form setup.
...@@ -297,7 +293,14 @@ class @Notes ...@@ -297,7 +293,14 @@ class @Notes
else else
previewButton.removeClass("turn-on").addClass "turn-off" previewButton.removeClass("turn-on").addClass "turn-off"
textarea.on 'focus', ->
$(this).closest('.md-area').addClass 'is-focused'
textarea.on 'blur', ->
$(this).closest('.md-area').removeClass 'is-focused'
autosize(textarea) autosize(textarea)
new Autosave textarea, [ new Autosave textarea, [
"Note" "Note"
form.find("#note_commit_id").val() form.find("#note_commit_id").val()
...@@ -307,7 +310,6 @@ class @Notes ...@@ -307,7 +310,6 @@ class @Notes
] ]
# remove notify commit author checkbox for non-commit notes # remove notify commit author checkbox for non-commit notes
form.find(".js-notify-commit-author").remove() if form.find("#note_noteable_type").val() isnt "Commit"
GitLab.GfmAutoComplete.setup() GitLab.GfmAutoComplete.setup()
new DropzoneInput(form) new DropzoneInput(form)
form.show() form.show()
...@@ -455,15 +457,15 @@ class @Notes ...@@ -455,15 +457,15 @@ class @Notes
Shows the note form below the notes. Shows the note form below the notes.
### ###
replyToDiscussionNote: (e) => replyToDiscussionNote: (e) =>
form = $(".js-new-note-form") form = @formClone.clone()
replyLink = $(e.target).closest(".js-discussion-reply-button") replyLink = $(e.target).closest(".js-discussion-reply-button")
replyLink.hide() replyLink.hide()
# insert the form after the button # insert the form after the button
form.clone().insertAfter replyLink replyLink.after form
# show the form # show the form
@setupDiscussionNoteForm(replyLink, replyLink.next("form")) @setupDiscussionNoteForm(replyLink, form)
### ###
Shows the diff or discussion form and does some setup on it. Shows the diff or discussion form and does some setup on it.
...@@ -488,7 +490,9 @@ class @Notes ...@@ -488,7 +490,9 @@ class @Notes
.text(form.find('.js-close-discussion-note-form').data('cancel-text')) .text(form.find('.js-close-discussion-note-form').data('cancel-text'))
@setupNoteForm form @setupNoteForm form
form.find(".js-note-text").focus() form.find(".js-note-text").focus()
form.addClass "js-discussion-note-form" form
.removeClass('js-main-target-form')
.addClass("discussion-form js-discussion-note-form")
### ###
Called when clicking on the "add a comment" button on the side of a diff line. Called when clicking on the "add a comment" button on the side of a diff line.
...@@ -498,9 +502,8 @@ class @Notes ...@@ -498,9 +502,8 @@ class @Notes
### ###
addDiffNote: (e) => addDiffNote: (e) =>
e.preventDefault() e.preventDefault()
link = e.currentTarget $link = $(e.currentTarget)
form = $(".js-new-note-form") row = $link.closest("tr")
row = $(link).closest("tr")
nextRow = row.next() nextRow = row.next()
hasNotes = nextRow.is(".notes_holder") hasNotes = nextRow.is(".notes_holder")
addForm = false addForm = false
...@@ -509,7 +512,7 @@ class @Notes ...@@ -509,7 +512,7 @@ class @Notes
# In parallel view, look inside the correct left/right pane # In parallel view, look inside the correct left/right pane
if @isParallelView() if @isParallelView()
lineType = $(link).data("lineType") lineType = $link.data("lineType")
targetContent += "." + lineType targetContent += "." + lineType
rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\"></td><td class=\"notes_content parallel old\"></td><td class=\"notes_line\"></td><td class=\"notes_content parallel new\"></td></tr>" rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\"></td><td class=\"notes_content parallel old\"></td><td class=\"notes_line\"></td><td class=\"notes_content parallel new\"></td></tr>"
...@@ -531,11 +534,11 @@ class @Notes ...@@ -531,11 +534,11 @@ class @Notes
addForm = true addForm = true
if addForm if addForm
newForm = form.clone() newForm = @formClone.clone()
newForm.appendTo row.next().find(targetContent) newForm.appendTo row.next().find(targetContent)
# show the form # show the form
@setupDiscussionNoteForm $(link), newForm @setupDiscussionNoteForm $link, newForm
### ###
Called in response to "cancel" on a diff note form. Called in response to "cancel" on a diff note form.
...@@ -560,7 +563,6 @@ class @Notes ...@@ -560,7 +563,6 @@ class @Notes
cancelDiscussionForm: (e) => cancelDiscussionForm: (e) =>
e.preventDefault() e.preventDefault()
form = $(".js-new-note-form")
form = $(e.target).closest(".js-discussion-note-form") form = $(e.target).closest(".js-discussion-note-form")
@removeDiscussionNoteForm(form) @removeDiscussionNoteForm(form)
......
...@@ -62,6 +62,8 @@ class @SearchAutocomplete ...@@ -62,6 +62,8 @@ class @SearchAutocomplete
search: search:
fields: ['text'] fields: ['text']
data: @getData.bind(@) data: @getData.bind(@)
selectable: true
clicked: @onClick.bind(@)
getData: (term, callback) -> getData: (term, callback) ->
_this = @ _this = @
...@@ -102,6 +104,8 @@ class @SearchAutocomplete ...@@ -102,6 +104,8 @@ class @SearchAutocomplete
lastCategory = suggestion.category lastCategory = suggestion.category
data.push data.push
id: "#{suggestion.category.toLowerCase()}-#{suggestion.id}"
category: suggestion.category
text: suggestion.label text: suggestion.label
url: suggestion.url url: suggestion.url
...@@ -133,12 +137,19 @@ class @SearchAutocomplete ...@@ -133,12 +137,19 @@ class @SearchAutocomplete
} }
bindEvents: -> bindEvents: ->
$(document).on 'click', @onDocumentClick
@searchInput.on 'keydown', @onSearchInputKeyDown @searchInput.on 'keydown', @onSearchInputKeyDown
@searchInput.on 'keyup', @onSearchInputKeyUp @searchInput.on 'keyup', @onSearchInputKeyUp
@searchInput.on 'click', @onSearchInputClick @searchInput.on 'click', @onSearchInputClick
@searchInput.on 'focus', @onSearchInputFocus @searchInput.on 'focus', @onSearchInputFocus
@searchInput.on 'blur', @onSearchInputBlur @clearInput.on 'click', @onClearInputClick
@clearInput.on 'click', @onRemoveLocationClick
onDocumentClick: (e) =>
# If clicking outside the search box
# And search input is not focused
# And we are not clicking inside a suggestion
if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).parents('ul').length
@onSearchInputBlur()
enableAutocomplete: -> enableAutocomplete: ->
# No need to enable anything if user is not logged in # No need to enable anything if user is not logged in
...@@ -181,6 +192,8 @@ class @SearchAutocomplete ...@@ -181,6 +192,8 @@ class @SearchAutocomplete
# We should display the menu only when input is not empty # We should display the menu only when input is not empty
@enableAutocomplete() @enableAutocomplete()
@wrap.toggleClass 'has-value', !!e.target.value
# Avoid falsy value to be returned # Avoid falsy value to be returned
return return
...@@ -189,27 +202,20 @@ class @SearchAutocomplete ...@@ -189,27 +202,20 @@ class @SearchAutocomplete
e.stopImmediatePropagation() e.stopImmediatePropagation()
onSearchInputFocus: => onSearchInputFocus: =>
@isFocused = true
@wrap.addClass('search-active') @wrap.addClass('search-active')
onRemoveLocationClick: (e) => onClearInputClick: (e) =>
e.preventDefault() e.preventDefault()
@removeLocationBadge()
@searchInput.val('').focus() @searchInput.val('').focus()
@skipBlurEvent = true
onSearchInputBlur: (e) => onSearchInputBlur: (e) =>
@skipBlurEvent = false @isFocused = false
# We should wait to make sure we are not clearing the input instead
setTimeout( =>
return if @skipBlurEvent
@wrap.removeClass('search-active') @wrap.removeClass('search-active')
# If input is blank then restore state # If input is blank then restore state
if @searchInput.val() is '' if @searchInput.val() is ''
@restoreOriginalState() @restoreOriginalState()
, 150)
addLocationBadge: (item) -> addLocationBadge: (item) ->
category = if item.category? then "#{item.category}: " else '' category = if item.category? then "#{item.category}: " else ''
...@@ -268,3 +274,23 @@ class @SearchAutocomplete ...@@ -268,3 +274,23 @@ class @SearchAutocomplete
<li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li>
</ul>" </ul>"
@dropdownContent.html(html) @dropdownContent.html(html)
onClick: (item, $el, e) ->
if location.pathname.indexOf(item.url) isnt -1
e.preventDefault()
if not @badgePresent
if item.category is 'Projects'
@projectInputEl.val(item.id)
@addLocationBadge(
value: 'This project'
)
if item.category is 'Groups'
@groupInputEl.val(item.id)
@addLocationBadge(
value: 'This group'
)
$el.removeClass('is-active')
@disableAutocomplete()
@searchInput.val('').focus()
...@@ -42,7 +42,7 @@ class @ZenMode ...@@ -42,7 +42,7 @@ class @ZenMode
$(e.currentTarget).trigger('zen_mode:leave') $(e.currentTarget).trigger('zen_mode:leave')
$(document).on 'zen_mode:enter', (e) => $(document).on 'zen_mode:enter', (e) =>
@enter(e.target.parentNode) @enter($(e.target).closest('.md-area').find('.zen-backdrop'))
$(document).on 'zen_mode:leave', (e) => $(document).on 'zen_mode:leave', (e) =>
@exit() @exit()
......
...@@ -125,13 +125,6 @@ p.time { ...@@ -125,13 +125,6 @@ p.time {
height: 150px; height: 150px;
} }
// Fixes alignment on notes.
.new_note {
label {
text-align: left;
}
}
// Fix issue with notes & lists creating a bunch of bottom borders. // Fix issue with notes & lists creating a bunch of bottom borders.
li.note { li.note {
img { max-width: 100% } img { max-width: 100% }
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
margin: 0; margin: 0;
text-align: left; text-align: left;
padding: 10px $gl-padding; padding: 10px $gl-padding;
word-wrap: break-word;
.file-actions { .file-actions {
float: right; float: right;
......
.div-dropzone-wrapper { .div-dropzone-wrapper {
.div-dropzone { .div-dropzone {
position: relative; position: relative;
padding: 0; margin-bottom: -5px;
border: 0;
margin-bottom: 5px;
.div-dropzone-focus { .div-dropzone-focus {
border-color: #66afe9 !important; border-color: #66afe9 !important;
...@@ -25,12 +23,10 @@ ...@@ -25,12 +23,10 @@
.div-dropzone-spinner { .div-dropzone-spinner {
position: absolute; position: absolute;
top: 100%; bottom: 10px;
left: 100%; right: 5px;
margin-top: -1.1em;
margin-left: -1.1em;
opacity: 0; opacity: 0;
font-size: 30px; font-size: 20px;
transition: opacity 200ms ease-in-out; transition: opacity 200ms ease-in-out;
} }
...@@ -65,17 +61,30 @@ ...@@ -65,17 +61,30 @@
position: relative; position: relative;
} }
.md-header {
.nav-links {
.active {
a {
border-bottom-color: #000;
}
}
a {
padding-top: 0;
line-height: 1;
}
}
}
.referenced-users { .referenced-users {
color: #4c4e54; color: #4c4e54;
padding-top: 10px; padding-top: 10px;
} }
.md-preview-holder { .md-preview-holder {
background: #fff; min-height: 167px;
border: 1px solid #ddd; padding: 10px 0;
min-height: 169px; overflow-x: auto;
padding: 5px;
box-shadow: none;
} }
.markdown-area { .markdown-area {
......
...@@ -250,7 +250,7 @@ a > code { ...@@ -250,7 +250,7 @@ a > code {
* Textareas intended for GFM * Textareas intended for GFM
* *
*/ */
textarea.js-gfm-input { .js-gfm-input {
font-family: $monospace_font; font-family: $monospace_font;
color: $gl-text-color; color: $gl-text-color;
} }
......
...@@ -104,9 +104,9 @@ $orange-light: rgba(252, 109, 38, 0.80); ...@@ -104,9 +104,9 @@ $orange-light: rgba(252, 109, 38, 0.80);
$orange-normal: #e75e40; $orange-normal: #e75e40;
$orange-dark: #ce5237; $orange-dark: #ce5237;
$red-light: #f06559; $red-light: #e52c5a;
$red-normal: #e52c5a; $red-normal: #d22852;
$red-dark: #d22852; $red-dark: darken($red-normal, 5%);
$border-white-light: #f1f2f4; $border-white-light: #f1f2f4;
$border-white-normal: #d6dae2; $border-white-normal: #d6dae2;
...@@ -128,9 +128,9 @@ $border-orange-light: #fc6d26; ...@@ -128,9 +128,9 @@ $border-orange-light: #fc6d26;
$border-orange-normal: #ce5237; $border-orange-normal: #ce5237;
$border-orange-dark: #c14e35; $border-orange-dark: #c14e35;
$border-red-light: #f24f41; $border-red-light: #d22852;
$border-red-normal: #d22852; $border-red-normal: #ca264f;
$border-red-dark: #ca264f; $border-red-dark: darken($border-red-normal, 5%);
$help-well-bg: #fafafa; $help-well-bg: #fafafa;
$help-well-border: #e5e5e5; $help-well-border: #e5e5e5;
...@@ -201,14 +201,14 @@ $award-emoji-new-btn-icon-color: #dcdcdc; ...@@ -201,14 +201,14 @@ $award-emoji-new-btn-icon-color: #dcdcdc;
/* /*
* Search Box * Search Box
*/ */
$search-input-border-color: $dropdown-input-focus-border; $search-input-border-color: rgba(#4688f1, .8);
$search-input-focus-shadow-color: $dropdown-input-focus-shadow; $search-input-focus-shadow-color: $dropdown-input-focus-shadow;
$search-input-width: $dropdown-width; $search-input-width: 244px;
$location-badge-color: #aaa; $location-badge-color: #aaa;
$location-badge-bg: $gray-normal; $location-badge-bg: $gray-normal;
$location-badge-active-bg: #4f91f8;
$location-icon-color: #e7e9ed; $location-icon-color: #e7e9ed;
$location-active-color: $gl-text-color; $location-icon-active-color: #807e7e;
$location-active-bg: $search-input-border-color;
/* /*
* Notes * Notes
...@@ -217,3 +217,9 @@ $notes-light-color: #8e8e8e; ...@@ -217,3 +217,9 @@ $notes-light-color: #8e8e8e;
$notes-action-color: #c3c3c3; $notes-action-color: #c3c3c3;
$notes-role-color: #8e8e8e; $notes-role-color: #8e8e8e;
$notes-role-border-color: #e4e4e4; $notes-role-border-color: #e4e4e4;
$note-disabled-comment-color: #b2b2b2;
$note-form-border-color: #e5e5e5;
$note-toolbar-color: #959494;
$zen-control-hover-color: #111;
.zennable { .zen-backdrop {
a.js-zen-enter {
color: $gl-gray;
position: absolute;
top: 0;
right: 4px;
line-height: 56px;
}
a.js-zen-leave {
display: none;
color: $gl-text-color;
position: absolute;
top: 10px;
right: 10px;
padding: 5px;
font-size: 36px;
&:hover {
color: #111;
}
}
.zen-backdrop {
&.fullscreen { &.fullscreen {
background-color: white; background-color: white;
position: fixed; position: fixed;
...@@ -47,15 +24,39 @@ ...@@ -47,15 +24,39 @@
margin: 0 auto; margin: 0 auto;
} }
a.js-zen-enter { .zen-control-leave {
display: none;
}
a.js-zen-leave {
display: block; display: block;
position: absolute; position: absolute;
top: 0; top: 0;
} }
} }
}
.zen-cotrol {
padding: 0;
color: #555;
background: none;
border: 0;
}
.zen-control-full {
color: $note-toolbar-color;
&:hover {
color: $gl-link-color;
text-decoration: none;
}
}
.zen-control-leave {
display: none;
color: $gl-text-color;
position: absolute;
right: 10px;
padding: 5px;
font-size: 36px;
&:hover {
color: $zen-control-hover-color;
} }
} }
...@@ -33,8 +33,12 @@ ...@@ -33,8 +33,12 @@
.description { .description {
margin-top: 6px; margin-top: 6px;
p:last-child { p {
overflow-x: auto;
&:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
} }
}
} }
...@@ -59,10 +59,15 @@ ...@@ -59,10 +59,15 @@
border-collapse: separate; border-collapse: separate;
margin: 0; margin: 0;
padding: 0; padding: 0;
.line_holder td { .line_holder td {
line-height: $code_line_height; line-height: $code_line_height;
font-size: $code_font_size; font-size: $code_font_size;
} }
td {
white-space: nowrap;
}
} }
tr.line_holder.parallel { tr.line_holder.parallel {
......
...@@ -195,42 +195,6 @@ ...@@ -195,42 +195,6 @@
line-height: 31px; line-height: 31px;
} }
.disabled-comment-area {
padding: 16px 0;
.disabled-profile {
width: 40px;
height: 40px;
background: $border-gray-dark;
border-radius: 20px;
display: inline-block;
margin-right: 10px;
}
.disabled-comment {
background: $gray-light;
display: inline-block;
vertical-align: top;
height: 200px;
border-radius: 4px;
border: 1px solid $border-gray-normal;
padding-top: 90px;
text-align: center;
right: 20px;
position: absolute;
left: 70px;
margin-bottom: 20px;
span {
color: #b2b2b2;
a {
color: $md-link-color;
}
}
}
}
.builds { .builds {
.table-holder { .table-holder {
overflow-x: scroll; overflow-x: scroll;
......
/** /**
* Note Form * Note Form
*/ */
.comment-btn {
@extend .btn-create;
}
.reply-btn { .reply-btn {
@extend .btn-primary; @extend .btn-primary;
margin: 10px $gl-padding; margin: 10px $gl-padding;
...@@ -17,16 +13,17 @@ ...@@ -17,16 +13,17 @@
} }
.diff-file, .diff-file,
.discussion { .discussion {
.new_note { .new-note {
margin: 0; margin: 0;
border: none; border: none;
} }
} }
.new_note {
.new-note {
display: none; display: none;
} }
.new_note, .note-edit-form { .new-note, .note-edit-form {
.note-form-actions { .note-form-actions {
margin-top: $gl-padding; margin-top: $gl-padding;
} }
...@@ -40,21 +37,18 @@ ...@@ -40,21 +37,18 @@
img { img {
max-width: 100%; max-width: 100%;
} }
}
.note_text { .note-textarea {
width: 100%; padding: 10px 0;
} font-family: $regular_font;
border: 0;
.comment-hints { &:focus {
margin-top: -12px; outline: 0;
} }
} }
/* loading indicator */
.notes-busy {
margin: 18px;
}
.note-image-attach { .note-image-attach {
@extend .col-md-4; @extend .col-md-4;
margin-left: 45px; margin-left: 45px;
...@@ -62,38 +56,29 @@ ...@@ -62,38 +56,29 @@
} }
.common-note-form { .common-note-form {
margin: 0; .md-area {
background: #fff; padding: $gl-padding-top $gl-padding;
padding: $gl-padding; border: 1px solid $note-form-border-color;
margin-left: -$gl-padding; border-radius: $border-radius-base;
margin-right: -$gl-padding;
margin-bottom: -$gl-padding;
}
.note-form-actions { &.is-focused {
.note-form-option { border-color: $focus-border-color;
margin-top: 8px; box-shadow: 0 0 2px rgba(#000, .2),
margin-left: 30px; 0 0 4px rgba($focus-border-color, .4);
@extend .pull-left;
}
.js-notify-commit-author { .comment-toolbar,
float: left; .nav-links {
border-color: $focus-border-color;
} }
.write-preview-btn {
// makes the "absolute" position for links relative to this
position: relative;
// preview/edit buttons
> a {
position: absolute;
right: 5px;
top: 8px;
} }
} }
} }
.discussion-form {
padding: $gl-padding-top $gl-padding;
background-color: #fff;
}
.note-edit-form { .note-edit-form {
display: none; display: none;
font-size: 15px; font-size: 15px;
...@@ -152,11 +137,49 @@ ...@@ -152,11 +137,49 @@
} }
} }
.comment-hints { .comment-toolbar {
color: #999; padding-top: $gl-padding-top;
background: #fff; color: $note-toolbar-color;
padding: 7px; border-top: 1px solid $border-color;
margin-top: -7px; }
border: 1px solid $border-color;
font-size: 13px; .toolbar-button {
padding: 0;
background: none;
border: 0;
font-size: 14px;
line-height: 16px;
&:hover,
&:focus {
color: $gl-link-color;
outline: 0;
}
@media (min-width: $screen-md-min) {
float: left;
margin-right: $gl-padding;
&:last-child {
float: right;
margin-right: 0;
}
}
}
.toolbar-button-icon {
position: relative;
top: 1px;
margin-right: 3px;
color: inherit;
font-size: 16px;
}
.toolbar-text {
font-size: 14px;
line-height: 16px;
@media (min-width: $screen-md-min) {
float: left;
}
} }
...@@ -20,6 +20,12 @@ ul.notes { ...@@ -20,6 +20,12 @@ ul.notes {
.timeline-content { .timeline-content {
margin-left: 55px; margin-left: 55px;
&.timeline-content-form {
@media (max-width: $screen-sm-max) {
margin-left: 0;
}
}
} }
.note-created-ago, .note-updated-at { .note-created-ago, .note-updated-at {
...@@ -76,7 +82,7 @@ ul.notes { ...@@ -76,7 +82,7 @@ ul.notes {
// On diffs code should wrap nicely and not overflow // On diffs code should wrap nicely and not overflow
pre { pre {
code { code {
white-space: pre-wrap; white-space: pre;
} }
} }
...@@ -149,7 +155,7 @@ ul.notes { ...@@ -149,7 +155,7 @@ ul.notes {
&.notes_content { &.notes_content {
background-color: #fff; background-color: #fff;
border-width: 1px 0; border-width: 1px 0;
padding-top: 0; padding: 0;
vertical-align: top; vertical-align: top;
&.parallel { &.parallel {
border-width: 1px; border-width: 1px;
...@@ -281,3 +287,21 @@ ul.notes { ...@@ -281,3 +287,21 @@ ul.notes {
} }
} }
} }
.disabled-comment {
margin-left: -$gl-padding-top;
margin-right: -$gl-padding-top;
background-color: $gray-light;
border-radius: $border-radius-base;
border: 1px solid $border-gray-normal;
color: $note-disabled-comment-color;
line-height: 200px;
.disabled-comment-text {
line-height: normal;
}
a {
color: $gl-link-color;
}
}
...@@ -315,7 +315,7 @@ pre.light-well { ...@@ -315,7 +315,7 @@ pre.light-well {
} }
.git-empty { .git-empty {
margin: 0 7px; margin: 0 7px 7px;
h5 { h5 {
color: #5c5d5e; color: #5c5d5e;
...@@ -401,7 +401,7 @@ pre.light-well { ...@@ -401,7 +401,7 @@ pre.light-well {
} }
.commit_short_id { .commit_short_id {
margin-right: 5px; margin: 0 5px;
color: $gl-link-color; color: $gl-link-color;
font-weight: 600; font-weight: 600;
} }
......
...@@ -135,17 +135,18 @@ ...@@ -135,17 +135,18 @@
.location-badge { .location-badge {
@include transition(all .15s); @include transition(all .15s);
background-color: $location-active-bg; background-color: $location-badge-active-bg;
color: $white-light; color: $white-light;
} }
.search-input-wrap { .search-input-wrap {
i { i {
color: $location-active-color; color: $location-icon-active-color;
}
} }
} }
&.has-location-badge { &.has-value {
.search-icon { .search-icon {
display: none; display: none;
} }
...@@ -155,7 +156,6 @@ ...@@ -155,7 +156,6 @@
display: block; display: block;
} }
} }
}
&.has-location-badge { &.has-location-badge {
.search-input-wrap { .search-input-wrap {
......
.container-fluid .content { .container-fluid {
.ci-status { .ci-status {
padding: 2px 7px; padding: 2px 7px;
margin-right: 5px; margin-right: 5px;
......
...@@ -5,7 +5,7 @@ class Admin::ProjectsController < Admin::ApplicationController ...@@ -5,7 +5,7 @@ class Admin::ProjectsController < Admin::ApplicationController
def index def index
@projects = Project.all @projects = Project.all
@projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present? @projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present?
@projects = @projects.where("visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present? @projects = @projects.where("projects.visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present?
@projects = @projects.with_push if params[:with_push].present? @projects = @projects.with_push if params[:with_push].present?
@projects = @projects.abandoned if params[:abandoned].present? @projects = @projects.abandoned if params[:abandoned].present?
@projects = @projects.non_archived unless params[:with_archived].present? @projects = @projects.non_archived unless params[:with_archived].present?
......
...@@ -55,7 +55,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -55,7 +55,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end end
else else
saml_user = Gitlab::Saml::User.new(oauth) saml_user = Gitlab::Saml::User.new(oauth)
saml_user.save saml_user.save if saml_user.changed?
@user = saml_user.gl_user @user = saml_user.gl_user
continue_login_process continue_login_process
......
...@@ -68,7 +68,9 @@ class Projects::ApplicationController < ApplicationController ...@@ -68,7 +68,9 @@ class Projects::ApplicationController < ApplicationController
end end
def require_non_empty_project def require_non_empty_project
redirect_to namespace_project_path(@project.namespace, @project) if @project.empty_repo? # Be sure to return status code 303 to avoid a double DELETE:
# http://api.rubyonrails.org/classes/ActionController/Redirecting.html
redirect_to namespace_project_path(@project.namespace, @project), status: 303 if @project.empty_repo?
end end
def require_branch_head def require_branch_head
......
class Projects::BadgesController < Projects::ApplicationController class Projects::BadgesController < Projects::ApplicationController
before_action :no_cache_headers layout 'project_settings'
before_action :authorize_admin_project!, only: [:index]
before_action :no_cache_headers, except: [:index]
def index
@ref = params[:ref] || @project.default_branch || 'master'
@build_badge = Gitlab::Badge::Build.new(@project, @ref)
end
def build def build
badge = Gitlab::Badge::Build.new(project, params[:ref]) badge = Gitlab::Badge::Build.new(project, params[:ref])
......
...@@ -48,7 +48,7 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -48,7 +48,7 @@ class Projects::BranchesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
redirect_to namespace_project_branches_path(@project.namespace, redirect_to namespace_project_branches_path(@project.namespace,
@project) @project), status: 303
end end
format.js { render status: status[:return_code] } format.js { render status: status[:return_code] }
end end
......
...@@ -94,9 +94,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -94,9 +94,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end end
def apply_import def apply_import
giver = Project.find(params[:source_project_id]) source_project = Project.find(params[:source_project_id])
status = @project.team.import(giver, current_user)
if can?(current_user, :read_project_member, source_project)
status = @project.team.import(source_project, current_user)
notice = status ? "Successfully imported" : "Import failed" notice = status ? "Successfully imported" : "Import failed"
else
return render_404
end
redirect_to(namespace_project_project_members_path(project.namespace, project), redirect_to(namespace_project_project_members_path(project.namespace, project),
notice: notice) notice: notice)
......
...@@ -24,6 +24,8 @@ class Projects::RefsController < Projects::ApplicationController ...@@ -24,6 +24,8 @@ class Projects::RefsController < Projects::ApplicationController
namespace_project_find_file_path(@project.namespace, @project, @id) namespace_project_find_file_path(@project.namespace, @project, @id)
when "graphs_commits" when "graphs_commits"
commits_namespace_project_graph_path(@project.namespace, @project, @id) commits_namespace_project_graph_path(@project.namespace, @project, @id)
when "badges"
namespace_project_badges_path(@project.namespace, @project, ref: @id)
else else
namespace_project_commits_path(@project.namespace, @project, @id) namespace_project_commits_path(@project.namespace, @project, @id)
end end
......
...@@ -88,6 +88,20 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -88,6 +88,20 @@ class Projects::WikisController < Projects::ApplicationController
) )
end end
def markdown_preview
text = params[:text]
ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user)
ext.analyze(text)
render json: {
body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki),
references: {
users: ext.users.map(&:username)
}
}
end
def git_access def git_access
end end
......
...@@ -40,6 +40,9 @@ class ProjectsController < Projects::ApplicationController ...@@ -40,6 +40,9 @@ class ProjectsController < Projects::ApplicationController
def update def update
status = ::Projects::UpdateService.new(@project, current_user, project_params).execute status = ::Projects::UpdateService.new(@project, current_user, project_params).execute
# Refresh the repo in case anything changed
@repository = project.repository
respond_to do |format| respond_to do |format|
if status if status
flash[:notice] = "Project '#{@project.name}' was successfully updated." flash[:notice] = "Project '#{@project.name}' was successfully updated."
......
...@@ -50,12 +50,15 @@ class BuildsEmailService < Service ...@@ -50,12 +50,15 @@ class BuildsEmailService < Service
def execute(push_data) def execute(push_data)
return unless supported_events.include?(push_data[:object_kind]) return unless supported_events.include?(push_data[:object_kind])
return unless should_build_be_notified?(push_data)
if should_build_be_notified?(push_data) recipients = all_recipients(push_data)
if recipients.any?
BuildEmailWorker.perform_async( BuildEmailWorker.perform_async(
push_data[:build_id], push_data[:build_id],
all_recipients(push_data), recipients,
push_data, push_data
) )
end end
end end
...@@ -84,7 +87,7 @@ class BuildsEmailService < Service ...@@ -84,7 +87,7 @@ class BuildsEmailService < Service
end end
def all_recipients(data) def all_recipients(data)
all_recipients = recipients.split(',') all_recipients = recipients.split(',').compact.reject(&:blank?)
if add_pusher? && data[:user][:email] if add_pusher? && data[:user][:email]
all_recipients << "#{data[:user][:email]}" all_recipients << "#{data[:user][:email]}"
......
...@@ -55,15 +55,15 @@ class GitPushService < BaseService ...@@ -55,15 +55,15 @@ class GitPushService < BaseService
end end
def update_main_language def update_main_language
# Performance can be bad so for now only check main_language once
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/14937
return if @project.main_language.present?
return unless is_default_branch? return unless is_default_branch?
return unless push_to_new_branch? || push_to_existing_branch? return unless push_to_new_branch? || push_to_existing_branch?
current_language = @project.repository.main_language current_language = @project.repository.main_language
@project.update_attributes(main_language: current_language)
unless current_language == @project.main_language
return @project.update_attributes(main_language: current_language)
end
true true
end end
......
- if controller.controller_path =~ /^groups/ - if controller.controller_path =~ /^groups/ && @group.persisted?
- label = 'This group' - label = 'This group'
- if controller.controller_path =~ /^projects/ - if controller.controller_path =~ /^projects/ && @project.persisted?
- label = 'This project' - label = 'This project'
.search.search-form{class: "#{'has-location-badge' if label.present?}"} .search.search-form{class: "#{'has-location-badge' if label.present?}"}
......
...@@ -51,8 +51,13 @@ ...@@ -51,8 +51,13 @@
= icon('code fw') = icon('code fw')
%span %span
Variables Variables
= nav_link path: 'triggers#index' do = nav_link(controller: :triggers) do
= link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do
= icon('retweet fw') = icon('retweet fw')
%span %span
Triggers Triggers
= nav_link(controller: :badges) do
= link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do
= icon('star-half-empty fw')
%span
Badges
...@@ -5,10 +5,14 @@ ...@@ -5,10 +5,14 @@
- content_for :scripts_body_top do - content_for :scripts_body_top do
- project = @target_project || @project - project = @target_project || @project
- if @project_wiki
- markdown_preview_path = namespace_project_wikis_markdown_preview_path(project.namespace, project)
- else
- markdown_preview_path = markdown_preview_namespace_project_path(project.namespace, project)
- if current_user - if current_user
:javascript :javascript
window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}"; window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
window.markdown_preview_path = "#{markdown_preview_namespace_project_path(project.namespace, project)}"; window.markdown_preview_path = "#{markdown_preview_path}";
- content_for :scripts_body do - content_for :scripts_body do
= render "layouts/init_auto_complete" if current_user = render "layouts/init_auto_complete" if current_user
......
.md-area .md-area
.md-header.clearfix .md-header
%ul.nav-links %ul.nav-links
%li.active %li.active
%a.js-md-write-button(href="#md-write-holder" tabindex="-1") %a.js-md-write-button{ href: "#md-write-holder" }
Write Write
%li %li
%a.js-md-preview-button(href="#md-preview-holder" tabindex="-1") %a.js-md-preview-button{ href: "#md-preview-holder" }
Preview Preview
%li.pull-right
%button.zen-cotrol.zen-control-full.js-zen-enter{ type: 'button' }
Go full screen
%div
.md-write-holder .md-write-holder
= yield = yield
.md.md-preview-holder.hide .md.md-preview-holder.js-md-preview.hide{class: (preview_class if defined?(preview_class))}
.js-md-preview{class: (preview_class if defined?(preview_class))}
- if defined?(referenced_users) && referenced_users - if defined?(referenced_users) && referenced_users
%div.referenced-users.hide %div.referenced-users.hide
......
.zennable .zen-backdrop
.zen-backdrop
- classes << ' js-gfm-input js-autosize markdown-area' - classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f - if defined?(f) && f
= f.text_area attr, class: classes = f.text_area attr, class: classes, placeholder: "Write a comment or drag your files here..."
- else - else
= text_area_tag attr, nil, class: classes = text_area_tag attr, nil, class: classes, placeholder: "Write a comment or drag your files here..."
%a.js-zen-enter(tabindex="-1" href="#") %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" }
= icon('expand')
Edit in fullscreen
%a.js-zen-leave(tabindex="-1" href="#")
= icon('compress') = icon('compress')
- page_title 'Badges'
- badges_path = namespace_project_badges_path(@project.namespace, @project)
- header_title project_title(@project, 'Badges', badges_path)
.prepend-top-10
.panel.panel-default
.panel-heading
%b Builds badge &middot;
= @build_badge.to_html
.pull-right
= render 'shared/ref_switcher', destination: 'badges'
.panel-body
.row
.col-md-2.text-center
Markdown
.col-md-10.code.js-syntax-highlight
= highlight('.md', @build_badge.to_markdown)
.row
%hr
.row
.col-md-2.text-center
HTML
.col-md-10.code.js-syntax-highlight
= highlight('.html', @build_badge.to_html)
...@@ -22,4 +22,6 @@ ...@@ -22,4 +22,6 @@
if(typeof merge_request_widget === 'undefined') { if(typeof merge_request_widget === 'undefined') {
merge_request_widget = new MergeRequestWidget(opts); merge_request_widget = new MergeRequestWidget(opts);
} else {
merge_request_widget.setOpts(opts);
} }
.note-edit-form .note-edit-form
= form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note js-quick-submit' } do |f| = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f|
= note_target_fields(note) = note_target_fields(note)
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
= render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field' = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field'
= render 'projects/notes/hints' = render 'projects/notes/hints'
.note-form-actions.clearfix .note-form-actions.clearfix
= f.submit 'Save Comment', class: 'btn btn-nr btn-save btn-grouped js-comment-button' = f.submit 'Save Comment', class: 'btn btn-nr btn-save btn-grouped js-comment-button'
= link_to 'Cancel', '#', class: 'btn btn-nr btn-cancel note-edit-cancel' %button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' }
Cancel
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f| = form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view = hidden_field_tag :view, diff_view
= hidden_field_tag :line_type = hidden_field_tag :line_type
= note_target_fields(@note) = note_target_fields(@note)
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= f.hidden_field :noteable_type = f.hidden_field :noteable_type
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text' = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text'
= render 'projects/notes/hints' = render 'projects/notes/hints'
.error-alert .error-alert
......
.comment-hints.clearfix .comment-toolbar.clearfix
.pull-left .toolbar-text
Styling with
= link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1 = link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1
tip: is supported
= random_markdown_tip %button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' }
.pull-right = icon('file-image-o', class: 'toolbar-button-icon')
= link_to '#', class: 'markdown-selector', tabindex: -1 do
= icon('paperclip')
Attach a file Attach a file
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
= access = access
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil-square-o') = icon('pencil-square-o')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete' do
= icon('trash-o') = icon('trash-o')
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text .note-text
......
%ul#notes-list.notes.main-notes-list.timeline %ul#notes-list.notes.main-notes-list.timeline
= render "projects/notes/notes" = render "projects/notes/notes"
.js-notes-busy %ul.notes.timeline
%li.timeline-entry
.js-main-target-form - if can? current_user, :create_note, @project
- if can? current_user, :create_note, @project .timeline-icon.hidden-xs.hidden-sm
%a.author_link{ href: user_path(current_user) }
= image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40'
.timeline-content.timeline-content-form
= render "projects/notes/form", view: diff_view = render "projects/notes/form", view: diff_view
- else - else
.disabled-comment-area .disabled-comment.text-center
.disabled-profile .disabled-comment-text.inline
.disabled-comment
%span
Please Please
= link_to "register",new_user_session_path = link_to "register",new_user_session_path
or or
......
...@@ -345,6 +345,8 @@ production: &base ...@@ -345,6 +345,8 @@ production: &base
# #
# - { name: 'saml', # - { name: 'saml',
# label: 'Our SAML Provider', # label: 'Our SAML Provider',
# groups_attribute: 'Groups',
# external_groups: ['Contractors', 'Freelancers'],
# args: { # args: {
# assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', # assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
# idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', # idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
...@@ -352,6 +354,7 @@ production: &base ...@@ -352,6 +354,7 @@ production: &base
# issuer: 'https://gitlab.example.com', # issuer: 'https://gitlab.example.com',
# name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' # name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
# } } # } }
#
# - { name: 'crowd', # - { name: 'crowd',
# args: { # args: {
# crowd_server_url: 'CROWD SERVER URL', # crowd_server_url: 'CROWD SERVER URL',
......
...@@ -3,5 +3,6 @@ Premailer::Rails.config.merge!( ...@@ -3,5 +3,6 @@ Premailer::Rails.config.merge!(
generate_text_part: false, generate_text_part: false,
preserve_styles: true, preserve_styles: true,
remove_comments: true, remove_comments: true,
remove_ids: true remove_ids: true,
remove_scripts: false
) )
...@@ -575,6 +575,7 @@ Rails.application.routes.draw do ...@@ -575,6 +575,7 @@ Rails.application.routes.draw do
# Order matters to give priority to these matches # Order matters to give priority to these matches
get '/wikis/git_access', to: 'wikis#git_access' get '/wikis/git_access', to: 'wikis#git_access'
get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages' get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
post '/wikis/markdown_preview', to:'wikis#markdown_preview'
post '/wikis', to: 'wikis#create' post '/wikis', to: 'wikis#create'
get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
...@@ -749,15 +750,16 @@ Rails.application.routes.draw do ...@@ -749,15 +750,16 @@ Rails.application.routes.draw do
end end
resources :runner_projects, only: [:create, :destroy] resources :runner_projects, only: [:create, :destroy]
resources :badges, only: [], path: 'badges/*ref', resources :badges, only: [:index] do
constraints: { ref: Gitlab::Regex.git_reference_regex } do
collection do collection do
scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
get :build, constraints: { format: /svg/ } get :build, constraints: { format: /svg/ }
end end
end end
end end
end end
end end
end
# Get all keys of user # Get all keys of user
get ':username.keys' => 'profiles/keys#get_keys' , constraints: { username: /.*/ } get ':username.keys' => 'profiles/keys#get_keys' , constraints: { username: /.*/ }
......
...@@ -7,8 +7,24 @@ Returns a list of project milestones. ...@@ -7,8 +7,24 @@ Returns a list of project milestones.
``` ```
GET /projects/:id/milestones GET /projects/:id/milestones
GET /projects/:id/milestones?iid=42 GET /projects/:id/milestones?iid=42
GET /projects/:id/milestones?state=active
GET /projects/:id/milestones?state=closed
``` ```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
| `iid` | integer | optional | Return only the milestone having the given `iid` |
| `state` | string | optional | Return only `active` or `closed` milestones` |
```bash
curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/milestones
```
Example Response:
```json ```json
[ [
{ {
...@@ -25,10 +41,6 @@ GET /projects/:id/milestones?iid=42 ...@@ -25,10 +41,6 @@ GET /projects/:id/milestones?iid=42
] ]
``` ```
Parameters:
- `id` (required) - The ID of a project
- `iid` (optional) - Return the milestone having the given `iid`
## Get single milestone ## Get single milestone
......
...@@ -69,6 +69,7 @@ GET /users ...@@ -69,6 +69,7 @@ GET /users
"state": "blocked", "state": "blocked",
"created_at": "2012-05-23T08:01:01Z", "created_at": "2012-05-23T08:01:01Z",
"bio": null, "bio": null,
"location": null,
"skype": "", "skype": "",
"linkedin": "", "linkedin": "",
"twitter": "", "twitter": "",
...@@ -126,6 +127,7 @@ Parameters: ...@@ -126,6 +127,7 @@ Parameters:
"created_at": "2012-05-23T08:00:58Z", "created_at": "2012-05-23T08:00:58Z",
"is_admin": false, "is_admin": false,
"bio": null, "bio": null,
"location": null,
"skype": "", "skype": "",
"linkedin": "", "linkedin": "",
"twitter": "", "twitter": "",
...@@ -154,6 +156,7 @@ Parameters: ...@@ -154,6 +156,7 @@ Parameters:
"confirmed_at": "2012-05-23T08:00:58Z", "confirmed_at": "2012-05-23T08:00:58Z",
"last_sign_in_at": "2015-03-23T08:00:58Z", "last_sign_in_at": "2015-03-23T08:00:58Z",
"bio": null, "bio": null,
"location": null,
"skype": "", "skype": "",
"linkedin": "", "linkedin": "",
"twitter": "", "twitter": "",
...@@ -191,6 +194,7 @@ Parameters: ...@@ -191,6 +194,7 @@ Parameters:
- `extern_uid` (optional) - External UID - `extern_uid` (optional) - External UID
- `provider` (optional) - External provider name - `provider` (optional) - External provider name
- `bio` (optional) - User's biography - `bio` (optional) - User's biography
- `location` (optional) - User's location
- `admin` (optional) - User is admin - true or false (default) - `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false - `can_create_group` (optional) - User can create groups - true or false
- `confirm` (optional) - Require confirmation - true (default) or false - `confirm` (optional) - Require confirmation - true (default) or false
...@@ -218,6 +222,7 @@ Parameters: ...@@ -218,6 +222,7 @@ Parameters:
- `extern_uid` - External UID - `extern_uid` - External UID
- `provider` - External provider name - `provider` - External provider name
- `bio` - User's biography - `bio` - User's biography
- `location` (optional) - User's location
- `admin` (optional) - User is admin - true or false (default) - `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false - `can_create_group` (optional) - User can create groups - true or false
- `external` (optional) - Flags the user as external - true or false(default) - `external` (optional) - Flags the user as external - true or false(default)
...@@ -260,6 +265,7 @@ GET /user ...@@ -260,6 +265,7 @@ GET /user
"state": "active", "state": "active",
"created_at": "2012-05-23T08:00:58Z", "created_at": "2012-05-23T08:00:58Z",
"bio": null, "bio": null,
"location": null,
"skype": "", "skype": "",
"linkedin": "", "linkedin": "",
"twitter": "", "twitter": "",
......
...@@ -38,7 +38,7 @@ services: ...@@ -38,7 +38,7 @@ services:
- postgres - postgres
before_script: before_script:
- bundle_install - bundle install
stages: stages:
- build - build
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
- [CI setup](ci_setup.md) for testing GitLab - [CI setup](ci_setup.md) for testing GitLab
- [Gotchas](gotchas.md) to avoid - [Gotchas](gotchas.md) to avoid
- [How to dump production data to staging](db_dump.md) - [How to dump production data to staging](db_dump.md)
- [Instrumentation](instrumentation.md)
- [Migration Style Guide](migration_style_guide.md) for creating safe migrations - [Migration Style Guide](migration_style_guide.md) for creating safe migrations
- [Rake tasks](rake_tasks.md) for development - [Rake tasks](rake_tasks.md) for development
- [Shell commands](shell_commands.md) in the GitLab codebase - [Shell commands](shell_commands.md) in the GitLab codebase
......
# Instrumenting Ruby Code
GitLab Performance Monitoring allows instrumenting of custom blocks of Ruby
code. This can be used to measure the time spent in a specific part of a larger
chunk of code. The resulting data is written to a separate series.
To start measuring a block of Ruby code you should use
`Gitlab::Metrics.measure` and give it a name for the series to store the data
in:
```ruby
Gitlab::Metrics.measure(:user_logins) do
...
end
```
The first argument of this method is the series name and should be plural. This
name will be prefixed with `rails_` or `sidekiq_` depending on whether the code
was run in the Rails application or one of the Sidekiq workers. In the
above example the final series names would be as follows:
- rails_user_logins
- sidekiq_user_logins
Series names should be plural as this keeps the naming style in line with the
other series names.
By default metrics measured using a block contain a single value, "duration",
which contains the number of milliseconds it took to execute the block. Custom
values can be added by passing a Hash as the 2nd argument. Custom tags can be
added by passing a Hash as the 3rd argument. A simple example is as follows:
```ruby
Gitlab::Metrics.measure(:example_series, { number: 10 }, { class: self.class.to_s }) do
...
end
```
...@@ -131,8 +131,75 @@ On the sign in page there should now be a SAML button below the regular sign in ...@@ -131,8 +131,75 @@ On the sign in page there should now be a SAML button below the regular sign in
Click the icon to begin the authentication process. If everything goes well the user Click the icon to begin the authentication process. If everything goes well the user
will be returned to GitLab and will be signed in. will be returned to GitLab and will be signed in.
## External Groups
>**Note:**
This setting is only available on GitLab 8.7 and above.
SAML login includes support for external groups. You can define in the SAML
settings which groups, to which your users belong in your IdP, you wish to be
marked as [external](../permissions/permissions.md).
### Requirements
First you need to tell GitLab where to look for group information. For this you
need to make sure that your IdP server sends a specific `AttributeStament` along
with the regular SAML response. Here is an example:
```xml
<saml:AttributeStatement>
<saml:Attribute Name="Groups">
<saml:AttributeValue xsi:type="xs:string">SecurityGroup</saml:AttributeValue>
<saml:AttributeValue xsi:type="xs:string">Developers</saml:AttributeValue>
<saml:AttributeValue xsi:type="xs:string">Designers</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
```
The name of the attribute can be anything you like, but it must contain the groups
to which a user belongs. In order to tell GitLab where to find these groups, you need
to add a `groups_attribute:` element to your SAML settings. You will also need to
tell GitLab which groups are external via the `external_groups:` element:
```yaml
{ name: 'saml',
label: 'Our SAML Provider',
groups_attribute: 'Groups',
external_groups: ['Freelancers', 'Interns'],
args: {
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
} }
```
## Customization ## Customization
### `auto_sign_in_with_provider`
You can add this setting to your GitLab configuration to automatically redirect you
to your SAML server for authentication, thus removing the need to click a button
before actually signing in.
For omnibus package:
```ruby
gitlab_rails['omniauth_auto_sign_in_with_provider'] = 'saml'
```
For installations from source:
```yaml
omniauth:
auto_sign_in_with_provider: saml
```
Please keep in mind that every sign in attempt will be redirected to the SAML server,
so you will not be able to sign in using local credentials. Make sure that at least one
of the SAML users has admin permissions.
### `attribute_statements` ### `attribute_statements`
>**Note:** >**Note:**
...@@ -205,6 +272,10 @@ To bypass this you can add `skip_before_action :verify_authenticity_token` to th ...@@ -205,6 +272,10 @@ To bypass this you can add `skip_before_action :verify_authenticity_token` to th
where it can then be seen in the usual logs, or as a flash message in the login where it can then be seen in the usual logs, or as a flash message in the login
screen. screen.
That file is located at `/opt/gitlab/embedded/service/gitlab-rails/app/controllers`
for Omnibus installations and by default on `/home/git/gitlab/app/controllers` for
installations from source.
### Invalid audience ### Invalid audience
This error means that the IdP doesn't recognize GitLab as a valid sender and This error means that the IdP doesn't recognize GitLab as a valid sender and
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
_GitLab uses the [Redcarpet Ruby library][redcarpet] for Markdown processing._ _GitLab uses the [Redcarpet Ruby library][redcarpet] for Markdown processing._
For GitLab we developed something we call "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. GitLab uses "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/).
You can use GFM in You can use GFM in
......
...@@ -52,10 +52,11 @@ documentation](../workflow/add-user/add-user.md). ...@@ -52,10 +52,11 @@ documentation](../workflow/add-user/add-user.md).
| Switch visibility level | | | | | ✓ | | Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ | | Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ | | Remove project | | | | | ✓ |
| Force push to protected branches | | | | | | | Force push to protected branches [^2] | | | | | |
| Remove protected branches | | | | | | | Remove protected branches [^2] | | | | | |
[^1]: If **Allow guest to access builds** is enabled in CI settings [^1]: If **Allow guest to access builds** is enabled in CI settings
[^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner
## Group ## Group
......
...@@ -125,7 +125,7 @@ module SharedDiffNote ...@@ -125,7 +125,7 @@ module SharedDiffNote
step 'I should only see one diff form' do step 'I should only see one diff form' do
page.within(diff_file_selector) do page.within(diff_file_selector) do
expect(page).to have_css("form.new_note", count: 1) expect(page).to have_css("form.new-note", count: 1)
end end
end end
...@@ -161,7 +161,7 @@ module SharedDiffNote ...@@ -161,7 +161,7 @@ module SharedDiffNote
step 'I should see a temporary diff comment form' do step 'I should see a temporary diff comment form' do
page.within(diff_file_selector) do page.within(diff_file_selector) do
expect(page).to have_css(".js-temp-notes-holder form.new_note") expect(page).to have_css(".js-temp-notes-holder form.new-note")
end end
end end
......
...@@ -2,7 +2,7 @@ module SharedNote ...@@ -2,7 +2,7 @@ module SharedNote
include Spinach::DSL include Spinach::DSL
step 'I delete a comment' do step 'I delete a comment' do
page.within('.notes') do page.within('.main-notes-list') do
find('.note').hover find('.note').hover
find(".js-note-delete").click find(".js-note-delete").click
end end
...@@ -128,7 +128,7 @@ module SharedNote ...@@ -128,7 +128,7 @@ module SharedNote
end end
step 'I edit the last comment with a +1' do step 'I edit the last comment with a +1' do
page.within(".notes") do page.within(".main-notes-list") do
find(".note").hover find(".note").hover
find('.js-note-edit').click find('.js-note-edit').click
end end
......
...@@ -64,7 +64,7 @@ module API ...@@ -64,7 +64,7 @@ module API
authorize_admin_project authorize_admin_project
@branch = user_project.repository.find_branch(params[:branch]) @branch = user_project.repository.find_branch(params[:branch])
not_found!("Branch does not exist") unless @branch not_found!("Branch") unless @branch
protected_branch = user_project.protected_branches.find_by(name: @branch.name) protected_branch = user_project.protected_branches.find_by(name: @branch.name)
protected_branch.destroy if protected_branch protected_branch.destroy if protected_branch
......
...@@ -15,7 +15,7 @@ module API ...@@ -15,7 +15,7 @@ module API
class User < UserBasic class User < UserBasic
expose :created_at expose :created_at
expose :is_admin?, as: :is_admin expose :is_admin?, as: :is_admin
expose :bio, :skype, :linkedin, :twitter, :website_url expose :bio, :location, :skype, :linkedin, :twitter, :website_url
end end
class Identity < Grape::Entity class Identity < Grape::Entity
......
...@@ -3,17 +3,33 @@ module API ...@@ -3,17 +3,33 @@ module API
class Milestones < Grape::API class Milestones < Grape::API
before { authenticate! } before { authenticate! }
helpers do
def filter_milestones_state(milestones, state)
case state
when 'active' then milestones.active
when 'closed' then milestones.closed
else milestones
end
end
end
resource :projects do resource :projects do
# Get a list of project milestones # Get a list of project milestones
# #
# Parameters: # Parameters:
# id (required) - The ID of a project # id (required) - The ID of a project
# state (optional) - Return "active" or "closed" milestones
# Example Request: # Example Request:
# GET /projects/:id/milestones # GET /projects/:id/milestones
# GET /projects/:id/milestones?state=active
# GET /projects/:id/milestones?state=closed
get ":id/milestones" do get ":id/milestones" do
authorize! :read_milestone, user_project authorize! :read_milestone, user_project
present paginate(user_project.milestones), with: Entities::Milestone milestones = user_project.milestones
milestones = filter_milestones_state(milestones, params[:state])
present paginate(milestones), with: Entities::Milestone
end end
# Get a single project milestone # Get a single project milestone
......
...@@ -58,6 +58,7 @@ module API ...@@ -58,6 +58,7 @@ module API
# extern_uid - External authentication provider UID # extern_uid - External authentication provider UID
# provider - External provider # provider - External provider
# bio - Bio # bio - Bio
# location - Location of the user
# admin - User is admin - true or false (default) # admin - User is admin - true or false (default)
# can_create_group - User can create groups - true or false # can_create_group - User can create groups - true or false
# confirm - Require user confirmation - true (default) or false # confirm - Require user confirmation - true (default) or false
...@@ -67,7 +68,7 @@ module API ...@@ -67,7 +68,7 @@ module API
post do post do
authenticated_as_admin! authenticated_as_admin!
required_attributes! [:email, :password, :name, :username] required_attributes! [:email, :password, :name, :username]
attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :can_create_group, :admin, :confirm, :external] attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external]
admin = attrs.delete(:admin) admin = attrs.delete(:admin)
confirm = !(attrs.delete(:confirm) =~ (/(false|f|no|0)$/i)) confirm = !(attrs.delete(:confirm) =~ (/(false|f|no|0)$/i))
user = User.build_user(attrs) user = User.build_user(attrs)
...@@ -106,6 +107,7 @@ module API ...@@ -106,6 +107,7 @@ module API
# website_url - Website url # website_url - Website url
# projects_limit - Limit projects each user can create # projects_limit - Limit projects each user can create
# bio - Bio # bio - Bio
# location - Location of the user
# admin - User is admin - true or false (default) # admin - User is admin - true or false (default)
# can_create_group - User can create groups - true or false # can_create_group - User can create groups - true or false
# external - Flags the user as external - true or false(default) # external - Flags the user as external - true or false(default)
...@@ -114,7 +116,7 @@ module API ...@@ -114,7 +116,7 @@ module API
put ":id" do put ":id" do
authenticated_as_admin! authenticated_as_admin!
attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :can_create_group, :admin, :external] attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external]
user = User.find(params[:id]) user = User.find(params[:id])
not_found!('User') unless user not_found!('User') unless user
......
...@@ -119,7 +119,7 @@ module Banzai ...@@ -119,7 +119,7 @@ module Banzai
elsif element_node?(node) elsif element_node?(node)
yield_valid_link(node) do |link, text| yield_valid_link(node) do |link, text|
if ref_pattern && link =~ /\A#{ref_pattern}/ if ref_pattern && link =~ /\A#{ref_pattern}\z/
replace_link_node_with_href(node, link) do replace_link_node_with_href(node, link) do
object_link_filter(link, ref_pattern, link_text: text) object_link_filter(link, ref_pattern, link_text: text)
end end
......
...@@ -118,7 +118,7 @@ module Banzai ...@@ -118,7 +118,7 @@ module Banzai
end end
if path if path
content_tag(:img, nil, src: path) content_tag(:img, nil, src: path, class: 'gfm')
end end
end end
...@@ -144,12 +144,18 @@ module Banzai ...@@ -144,12 +144,18 @@ module Banzai
# if it is not. # if it is not.
def process_page_link_tag(parts) def process_page_link_tag(parts)
if parts.size == 1 if parts.size == 1
url = parts[0].strip reference = parts[0].strip
else else
name, url = *parts.compact.map(&:strip) name, reference = *parts.compact.map(&:strip)
end end
content_tag(:a, name || url, href: url) if url?(reference)
href = reference
else
href = ::File.join(project_wiki_base_path, reference)
end
content_tag(:a, name || reference, href: href, class: 'gfm')
end end
def project_wiki def project_wiki
......
require 'uri'
module Banzai
module Filter
# HTML filter that "fixes" relative links to files in a repository.
#
# Context options:
# :project_wiki
class WikiLinkFilter < HTML::Pipeline::Filter
def call
return doc unless project_wiki?
doc.search('a:not(.gfm)').each do |el|
process_link_attr el.attribute('href')
end
doc
end
protected
def project_wiki?
!context[:project_wiki].nil?
end
def process_link_attr(html_attr)
return if html_attr.blank? || file_reference?(html_attr)
uri = URI(html_attr.value)
if uri.relative? && uri.path.present?
html_attr.value = rebuild_wiki_uri(uri).to_s
end
rescue URI::Error
# noop
end
def rebuild_wiki_uri(uri)
uri.path = ::File.join(project_wiki_base_path, uri.path)
uri
end
def file_reference?(html_attr)
!File.extname(html_attr.value).blank?
end
def project_wiki
context[:project_wiki]
end
def project_wiki_base_path
project_wiki && project_wiki.wiki_base_path
end
end
end
end
...@@ -2,8 +2,10 @@ module Banzai ...@@ -2,8 +2,10 @@ module Banzai
module Pipeline module Pipeline
class WikiPipeline < FullPipeline class WikiPipeline < FullPipeline
def self.filters def self.filters
@filters ||= super.insert_after(Filter::TableOfContentsFilter, @filters ||= begin
Filter::GollumTagsFilter) super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter)
.insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter)
end
end end
end end
end end
......
...@@ -4,14 +4,15 @@ module Gitlab ...@@ -4,14 +4,15 @@ module Gitlab
# Build badge # Build badge
# #
class Build class Build
include Gitlab::Application.routes.url_helpers
include ActionView::Helpers::AssetTagHelper
include ActionView::Helpers::UrlHelper
def initialize(project, ref) def initialize(project, ref)
@project, @ref = project, ref
@image = ::Ci::ImageForBuildService.new.execute(project, ref: ref) @image = ::Ci::ImageForBuildService.new.execute(project, ref: ref)
end end
def to_s
@image[:name].sub(/\.svg$/, '')
end
def type def type
'image/svg+xml' 'image/svg+xml'
end end
...@@ -19,6 +20,27 @@ module Gitlab ...@@ -19,6 +20,27 @@ module Gitlab
def data def data
File.read(@image[:path]) File.read(@image[:path])
end end
def to_s
@image[:name].sub(/\.svg$/, '')
end
def to_html
link_to(image_tag(image_url, alt: 'build status'), link_url)
end
def to_markdown
"[![build status](#{image_url})](#{link_url})"
end
def image_url
build_namespace_project_badges_url(@project.namespace,
@project, @ref, format: :svg)
end
def link_url
namespace_project_commits_url(@project.namespace, @project, id: @ref)
end
end end
end end
end end
...@@ -33,7 +33,10 @@ module Gitlab ...@@ -33,7 +33,10 @@ module Gitlab
def allowed? def allowed?
if ldap_user if ldap_user
return true unless ldap_config.active_directory unless ldap_config.active_directory
user.activate if user.ldap_blocked?
return true
end
# Block user in GitLab if he/she was blocked in AD # Block user in GitLab if he/she was blocked in AD
if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter) if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
......
...@@ -70,6 +70,32 @@ module Gitlab ...@@ -70,6 +70,32 @@ module Gitlab
value.to_s.gsub('=', '\\=') value.to_s.gsub('=', '\\=')
end end
# Measures the execution time of a block.
#
# Example:
#
# Gitlab::Metrics.measure(:find_by_username_timings) do
# User.find_by_username(some_username)
# end
#
# series - The name of the series to store the data in.
# values - A Hash containing extra values to add to the metric.
# tags - A Hash containing extra tags to add to the metric.
#
# Returns the value yielded by the supplied block.
def self.measure(series, values = {}, tags = {})
return yield unless Transaction.current
start = Time.now.to_f
retval = yield
duration = (Time.now.to_f - start) * 1000.0
values = values.merge(duration: duration)
Transaction.current.add_metric(series, values, tags)
retval
end
# When enabled this should be set before being used as the usual pattern # When enabled this should be set before being used as the usual pattern
# "@foo ||= bar" is _not_ thread-safe. # "@foo ||= bar" is _not_ thread-safe.
if enabled? if enabled?
......
module Gitlab
module Saml
class AuthHash < Gitlab::OAuth::AuthHash
def groups
get_raw(Gitlab::Saml::Config.groups)
end
private
def get_raw(key)
# Needs to call `all` because of https://git.io/vVo4u
# otherwise just the first value is returned
auth_hash.extra[:raw_info].all[key]
end
end
end
end
module Gitlab
module Saml
class Config
class << self
def options
Gitlab.config.omniauth.providers.find { |provider| provider.name == 'saml' }
end
def groups
options[:groups_attribute]
end
def external_groups
options[:external_groups]
end
end
end
end
end
...@@ -18,7 +18,7 @@ module Gitlab ...@@ -18,7 +18,7 @@ module Gitlab
@user ||= find_or_create_ldap_user @user ||= find_or_create_ldap_user
end end
if auto_link_saml_enabled? if auto_link_saml_user?
@user ||= find_by_email @user ||= find_by_email
end end
...@@ -26,6 +26,16 @@ module Gitlab ...@@ -26,6 +26,16 @@ module Gitlab
@user ||= build_new_user @user ||= build_new_user
end end
if external_users_enabled?
# Check if there is overlap between the user's groups and the external groups
# setting then set user as external or internal.
if (auth_hash.groups & Gitlab::Saml::Config.external_groups).empty?
@user.external = false
else
@user.external = true
end
end
@user @user
end end
...@@ -37,11 +47,23 @@ module Gitlab ...@@ -37,11 +47,23 @@ module Gitlab
end end
end end
def changed?
gl_user.changed? || gl_user.identities.any?(&:changed?)
end
protected protected
def auto_link_saml_enabled? def auto_link_saml_user?
Gitlab.config.omniauth.auto_link_saml_user Gitlab.config.omniauth.auto_link_saml_user
end end
def external_users_enabled?
!Gitlab::Saml::Config.external_groups.nil?
end
def auth_hash=(auth_hash)
@auth_hash = Gitlab::Saml::AuthHash.new(auth_hash)
end
end end
end end
end end
require 'spec_helper'
describe Admin::ProjectsController do
let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
before do
sign_in(create(:admin))
end
describe 'GET /projects' do
render_views
it 'retrieves the project for the given visibility level' do
get :index, visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]
expect(response.body).to match(project.name)
end
it 'does not retrieve the project' do
get :index, visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]
expect(response.body).to_not match(project.name)
end
end
end
...@@ -93,6 +93,20 @@ describe Projects::BranchesController do ...@@ -93,6 +93,20 @@ describe Projects::BranchesController do
end end
end end
describe "POST destroy with HTML format" do
render_views
it 'returns 303' do
post :destroy,
format: :html,
id: 'foo/bar/baz',
namespace_id: project.namespace.to_param,
project_id: project.to_param
expect(response.status).to eq(303)
end
end
describe "POST destroy" do describe "POST destroy" do
render_views render_views
......
require('spec_helper')
describe Projects::ProjectMembersController do
let(:project) { create(:project) }
let(:another_project) { create(:project, :private) }
let(:user) { create(:user) }
let(:member) { create(:user) }
before do
project.team << [user, :master]
another_project.team << [member, :guest]
sign_in(user)
end
describe '#apply_import' do
shared_context 'import applied' do
before do
post(:apply_import, namespace_id: project.namespace.to_param,
project_id: project.to_param,
source_project_id: another_project.id)
end
end
context 'when user can access source project members' do
before { another_project.team << [user, :guest] }
include_context 'import applied'
it 'imports source project members' do
expect(project.team_members).to include member
expect(response).to set_flash.to 'Successfully imported'
expect(response).to redirect_to(
namespace_project_project_members_path(project.namespace, project)
)
end
end
context 'when user is not member of a source project' do
include_context 'import applied'
it 'does not import team members' do
expect(project.team_members).to_not include member
end
it 'responds with not found' do
expect(response.status).to eq 404
end
end
end
end
...@@ -83,6 +83,28 @@ describe ProjectsController do ...@@ -83,6 +83,28 @@ describe ProjectsController do
end end
end end
describe "#update" do
render_views
let(:admin) { create(:admin) }
it "sets the repository to the right path after a rename" do
new_path = 'renamed_path'
project_params = { path: new_path }
controller.instance_variable_set(:@project, project)
sign_in(admin)
put :update,
namespace_id: project.namespace.to_param,
id: project.id,
project: project_params
expect(project.repository.path).to include(new_path)
expect(assigns(:repository).path).to eq(project.repository.path)
expect(response.status).to eq(200)
end
end
describe "#destroy" do describe "#destroy" do
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
......
require 'rails_helper'
describe 'Filter issues', feature: true do
let!(:project) { create(:project) }
let!(:user) { create(:user)}
let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
before do
project.team << [user, :master]
login_as(user)
end
describe 'Filter issues for assignee from issues#index' do
before do
visit namespace_project_issues_path(project.namespace, project)
find('.js-assignee-search').click
find('.dropdown-menu-user-link', text: user.username).click
sleep 2
end
context 'assignee', js: true do
it 'should update to current user' do
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
end
it 'should not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
end
it 'should not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
end
end
end
describe 'Filter issues for milestone from issues#index' do
before do
visit namespace_project_issues_path(project.namespace, project)
find('.js-milestone-select').click
find('.milestone-filter .dropdown-content a', text: milestone.title).click
sleep 2
end
context 'milestone', js: true do
it 'should update to current milestone' do
expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
end
it 'should not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
end
it 'should not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
end
end
end
describe 'Filter issues for assignee and label from issues#index' do
before do
visit namespace_project_issues_path(project.namespace, project)
find('.js-assignee-search').click
find('.dropdown-menu-user-link', text: user.username).click
sleep 2
find('.js-label-select').click
find('.dropdown-menu-labels .dropdown-content a', text: label.title).click
sleep 2
end
context 'assignee and label', js: true do
it 'should update to current assignee and label' do
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
end
it 'should not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
end
it 'should not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
end
end
end
end
...@@ -22,7 +22,7 @@ describe 'Issues', feature: true do ...@@ -22,7 +22,7 @@ describe 'Issues', feature: true do
before do before do
visit edit_namespace_project_issue_path(project.namespace, project, issue) visit edit_namespace_project_issue_path(project.namespace, project, issue)
click_link "Edit" click_button "Go full screen"
end end
it 'should open new issue popup' do it 'should open new issue popup' do
......
...@@ -39,7 +39,7 @@ describe 'GitLab Markdown', feature: true do ...@@ -39,7 +39,7 @@ describe 'GitLab Markdown', feature: true do
end end
def doc(html = @html) def doc(html = @html)
Nokogiri::HTML::DocumentFragment.parse(html) @doc ||= Nokogiri::HTML::DocumentFragment.parse(html)
end end
# Shared behavior that all pipelines should exhibit # Shared behavior that all pipelines should exhibit
...@@ -230,6 +230,7 @@ describe 'GitLab Markdown', feature: true do ...@@ -230,6 +230,7 @@ describe 'GitLab Markdown', feature: true do
file = Gollum::File.new(@project_wiki.wiki) file = Gollum::File.new(@project_wiki.wiki)
expect(file).to receive(:path).and_return('images/example.jpg') expect(file).to receive(:path).and_return('images/example.jpg')
expect(@project_wiki).to receive(:find_file).with('images/example.jpg').and_return(file) expect(@project_wiki).to receive(:find_file).with('images/example.jpg').and_return(file)
allow(@project_wiki).to receive(:wiki_base_path) { '/namespace1/gitlabhq/wikis' }
@html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki }) @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki })
end end
......
...@@ -152,7 +152,7 @@ describe 'Comments', feature: true do ...@@ -152,7 +152,7 @@ describe 'Comments', feature: true do
it 'has .new_note css class' do it 'has .new_note css class' do
page.within('.js-temp-notes-holder') do page.within('.js-temp-notes-holder') do
expect(subject).to have_css('.new_note') expect(subject).to have_css('.new-note')
end end
end end
end end
...@@ -225,6 +225,6 @@ describe 'Comments', feature: true do ...@@ -225,6 +225,6 @@ describe 'Comments', feature: true do
end end
def click_diff_line(data = line_code) def click_diff_line(data = line_code)
page.find(%Q{button[data-line-code="#{data}"]}, visible: false).click execute_script("$('button[data-line-code=\"#{data}\"]').click()")
end end
end end
require 'spec_helper'
feature 'list of badges' do
include Select2Helper
background do
user = create(:user)
project = create(:project)
project.team << [user, :master]
login_as(user)
visit edit_namespace_project_path(project.namespace, project)
end
scenario 'user displays list of badges' do
click_link 'Badges'
expect(page).to have_content 'build status'
expect(page).to have_content 'Markdown'
expect(page).to have_content 'HTML'
expect(page).to have_css('.highlight', count: 2)
expect(page).to have_xpath("//img[@alt='build status']")
page.within('.highlight', match: :first) do
expect(page).to have_content 'badges/master/build.svg'
end
end
scenario 'user changes current ref on badges list page', js: true do
click_link 'Badges'
select2('improve/awesome', from: '#ref')
expect(page).to have_content 'badges/improve/awesome/build.svg'
end
end
.zennable .md-area
.zen-backdrop .zen-backdrop
%textarea#note_note.js-gfm-input.markdown-area %textarea#note_note.js-gfm-input.markdown-area
%a.js-zen-enter(tabindex="-1" href="#") %a.js-zen-enter(tabindex="-1" href="#")
......
...@@ -29,7 +29,7 @@ describe 'reopen/close issue', -> ...@@ -29,7 +29,7 @@ describe 'reopen/close issue', ->
spyOn(jQuery, 'ajax').and.callFake (req) -> spyOn(jQuery, 'ajax').and.callFake (req) ->
expect(req.type).toBe('PUT') expect(req.type).toBe('PUT')
expect(req.url).toBe('http://gitlab.com/issues/6/close') expect(req.url).toBe('http://gitlab.com/issues/6/close')
req.success saved: true req.success id: 34
$btnClose = $('a.btn-close') $btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen') $btnReopen = $('a.btn-reopen')
...@@ -94,7 +94,7 @@ describe 'reopen/close issue', -> ...@@ -94,7 +94,7 @@ describe 'reopen/close issue', ->
spyOn(jQuery, 'ajax').and.callFake (req) -> spyOn(jQuery, 'ajax').and.callFake (req) ->
expect(req.type).toBe('PUT') expect(req.type).toBe('PUT')
expect(req.url).toBe('http://gitlab.com/issues/6/reopen') expect(req.url).toBe('http://gitlab.com/issues/6/reopen')
req.success saved: true req.success id: 34
$btnClose = $('a.btn-close') $btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen') $btnReopen = $('a.btn-reopen')
......
...@@ -70,20 +70,22 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do ...@@ -70,20 +70,22 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do
end end
context 'linking internal resources' do context 'linking internal resources' do
it "the created link's text will be equal to the resource's text" do it "the created link's text includes the resource's text and wiki base path" do
tag = '[[wiki-slug]]' tag = '[[wiki-slug]]'
doc = filter("See #{tag}", project_wiki: project_wiki) doc = filter("See #{tag}", project_wiki: project_wiki)
expected_path = ::File.join(project_wiki.wiki_base_path, 'wiki-slug')
expect(doc.at_css('a').text).to eq 'wiki-slug' expect(doc.at_css('a').text).to eq 'wiki-slug'
expect(doc.at_css('a')['href']).to eq 'wiki-slug' expect(doc.at_css('a')['href']).to eq expected_path
end end
it "the created link's text will be link-text" do it "the created link's text will be link-text" do
tag = '[[link-text|wiki-slug]]' tag = '[[link-text|wiki-slug]]'
doc = filter("See #{tag}", project_wiki: project_wiki) doc = filter("See #{tag}", project_wiki: project_wiki)
expected_path = ::File.join(project_wiki.wiki_base_path, 'wiki-slug')
expect(doc.at_css('a').text).to eq 'link-text' expect(doc.at_css('a').text).to eq 'link-text'
expect(doc.at_css('a')['href']).to eq 'wiki-slug' expect(doc.at_css('a')['href']).to eq expected_path
end end
end end
......
...@@ -95,6 +95,14 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do ...@@ -95,6 +95,14 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
result = reference_pipeline_result("Fixed #{reference}") result = reference_pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue] expect(result[:references][:issue]).to eq [issue]
end end
it 'does not process links containing issue numbers followed by text' do
href = "#{reference}st"
doc = reference_filter("<a href='#{href}'></a>")
link = doc.css('a').first.attr('href')
expect(link).to eq(href)
end
end end
context 'cross-project reference' do context 'cross-project reference' do
......
...@@ -11,7 +11,7 @@ describe Banzai::Pipeline::WikiPipeline do ...@@ -11,7 +11,7 @@ describe Banzai::Pipeline::WikiPipeline do
Foo Foo
MD MD
result = described_class.call(markdown, project: spy, project_wiki: double) result = described_class.call(markdown, project: spy, project_wiki: spy)
aggregate_failures do aggregate_failures do
expect(result[:output].text).not_to include '[[' expect(result[:output].text).not_to include '[['
...@@ -29,7 +29,7 @@ describe Banzai::Pipeline::WikiPipeline do ...@@ -29,7 +29,7 @@ describe Banzai::Pipeline::WikiPipeline do
Foo Foo
MD MD
output = described_class.to_html(markdown, project: spy, project_wiki: double) output = described_class.to_html(markdown, project: spy, project_wiki: spy)
expect(output).to include('[[<em>toc</em>]]') expect(output).to include('[[<em>toc</em>]]')
end end
...@@ -42,7 +42,7 @@ describe Banzai::Pipeline::WikiPipeline do ...@@ -42,7 +42,7 @@ describe Banzai::Pipeline::WikiPipeline do
Foo Foo
MD MD
output = described_class.to_html(markdown, project: spy, project_wiki: double) output = described_class.to_html(markdown, project: spy, project_wiki: spy)
aggregate_failures do aggregate_failures do
expect(output).not_to include('<ul>') expect(output).not_to include('<ul>')
......
...@@ -3,13 +3,44 @@ require 'spec_helper' ...@@ -3,13 +3,44 @@ require 'spec_helper'
describe Gitlab::Badge::Build do describe Gitlab::Badge::Build do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:sha) { project.commit.sha } let(:sha) { project.commit.sha }
let(:badge) { described_class.new(project, 'master') } let(:branch) { 'master' }
let(:badge) { described_class.new(project, branch) }
describe '#type' do describe '#type' do
subject { badge.type } subject { badge.type }
it { is_expected.to eq 'image/svg+xml' } it { is_expected.to eq 'image/svg+xml' }
end end
describe '#to_html' do
let(:html) { Nokogiri::HTML.parse(badge.to_html) }
let(:a_href) { html.at('a') }
it 'points to link' do
expect(a_href[:href]).to eq badge.link_url
end
it 'contains clickable image' do
expect(a_href.children.first.name).to eq 'img'
end
end
describe '#to_markdown' do
subject { badge.to_markdown }
it { is_expected.to include badge.image_url }
it { is_expected.to include badge.link_url }
end
describe '#image_url' do
subject { badge.image_url }
it { is_expected.to include "badges/#{branch}/build.svg" }
end
describe '#link_url' do
subject { badge.link_url }
it { is_expected.to include "commits/#{branch}" }
end
context 'build exists' do context 'build exists' do
let(:ci_commit) { create(:ci_commit, project: project, sha: sha) } let(:ci_commit) { create(:ci_commit, project: project, sha: sha) }
let!(:build) { create(:ci_build, commit: ci_commit) } let!(:build) { create(:ci_build, commit: ci_commit) }
......
...@@ -33,7 +33,7 @@ describe Gitlab::LDAP::Access, lib: true do ...@@ -33,7 +33,7 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
it 'should block user in GitLab' do it 'blocks user in GitLab' do
access.allowed? access.allowed?
expect(user).to be_blocked expect(user).to be_blocked
expect(user).to be_ldap_blocked expect(user).to be_ldap_blocked
...@@ -78,6 +78,31 @@ describe Gitlab::LDAP::Access, lib: true do ...@@ -78,6 +78,31 @@ describe Gitlab::LDAP::Access, lib: true do
end end
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
context 'when user cannot be found' do
before do
allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(nil)
end
it { is_expected.to be_falsey }
it 'blocks user in GitLab' do
access.allowed?
expect(user).to be_blocked
expect(user).to be_ldap_blocked
end
end
context 'when user was previously ldap_blocked' do
before do
user.ldap_block
end
it 'unblocks the user if it exists' do
access.allowed?
expect(user).not_to be_blocked
end
end
end end
end end
end end
......
...@@ -13,7 +13,7 @@ describe Gitlab::Metrics do ...@@ -13,7 +13,7 @@ describe Gitlab::Metrics do
end end
end end
describe '#submit_metrics' do describe '.submit_metrics' do
it 'prepares and writes the metrics to InfluxDB' do it 'prepares and writes the metrics to InfluxDB' do
connection = double(:connection) connection = double(:connection)
pool = double(:pool) pool = double(:pool)
...@@ -26,7 +26,7 @@ describe Gitlab::Metrics do ...@@ -26,7 +26,7 @@ describe Gitlab::Metrics do
end end
end end
describe '#prepare_metrics' do describe '.prepare_metrics' do
it 'returns a Hash with the keys as Symbols' do it 'returns a Hash with the keys as Symbols' do
metrics = described_class. metrics = described_class.
prepare_metrics([{ 'values' => {}, 'tags' => {} }]) prepare_metrics([{ 'values' => {}, 'tags' => {} }])
...@@ -51,7 +51,7 @@ describe Gitlab::Metrics do ...@@ -51,7 +51,7 @@ describe Gitlab::Metrics do
end end
end end
describe '#escape_value' do describe '.escape_value' do
it 'escapes an equals sign' do it 'escapes an equals sign' do
expect(described_class.escape_value('foo=')).to eq('foo\\=') expect(described_class.escape_value('foo=')).to eq('foo\\=')
end end
...@@ -60,4 +60,45 @@ describe Gitlab::Metrics do ...@@ -60,4 +60,45 @@ describe Gitlab::Metrics do
expect(described_class.escape_value(10)).to eq('10') expect(described_class.escape_value(10)).to eq('10')
end end
end end
describe '.measure' do
context 'without a transaction' do
it 'returns the return value of the block' do
val = Gitlab::Metrics.measure(:foo) { 10 }
expect(val).to eq(10)
end
end
context 'with a transaction' do
let(:transaction) { Gitlab::Metrics::Transaction.new }
before do
allow(Gitlab::Metrics::Transaction).to receive(:current).
and_return(transaction)
end
it 'adds a metric to the current transaction' do
expect(transaction).to receive(:add_metric).
with(:foo, { duration: a_kind_of(Numeric) }, { tag: 'value' })
Gitlab::Metrics.measure(:foo, {}, tag: 'value') { 10 }
end
it 'supports adding of custom values' do
values = { duration: a_kind_of(Numeric), number: 10 }
expect(transaction).to receive(:add_metric).
with(:foo, values, { tag: 'value' })
Gitlab::Metrics.measure(:foo, { number: 10 }, tag: 'value') { 10 }
end
it 'returns the return value of the block' do
val = Gitlab::Metrics.measure(:foo) { 10 }
expect(val).to eq(10)
end
end
end
end end
...@@ -5,7 +5,7 @@ describe Gitlab::Saml::User, lib: true do ...@@ -5,7 +5,7 @@ describe Gitlab::Saml::User, lib: true do
let(:gl_user) { saml_user.gl_user } let(:gl_user) { saml_user.gl_user }
let(:uid) { 'my-uid' } let(:uid) { 'my-uid' }
let(:provider) { 'saml' } let(:provider) { 'saml' }
let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) } let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new({ 'groups' => %w(Developers Freelancers Designers) }) }) }
let(:info_hash) do let(:info_hash) do
{ {
name: 'John', name: 'John',
...@@ -23,10 +23,20 @@ describe Gitlab::Saml::User, lib: true do ...@@ -23,10 +23,20 @@ describe Gitlab::Saml::User, lib: true do
allow(Gitlab::LDAP::Config).to receive_messages(messages) allow(Gitlab::LDAP::Config).to receive_messages(messages)
end end
def stub_basic_saml_config
allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', args: {} } })
end
def stub_saml_group_config(groups)
allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } })
end
before { stub_basic_saml_config }
describe 'account exists on server' do describe 'account exists on server' do
before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) } before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) }
context 'and should bind with SAML' do
let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') } let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') }
context 'and should bind with SAML' do
it 'adds the SAML identity to the existing user' do it 'adds the SAML identity to the existing user' do
saml_user.save saml_user.save
expect(gl_user).to be_valid expect(gl_user).to be_valid
...@@ -36,6 +46,35 @@ describe Gitlab::Saml::User, lib: true do ...@@ -36,6 +46,35 @@ describe Gitlab::Saml::User, lib: true do
expect(identity.provider).to eql 'saml' expect(identity.provider).to eql 'saml'
end end
end end
context 'external groups' do
context 'are defined' do
it 'marks the user as external' do
stub_saml_group_config(%w(Freelancers))
saml_user.save
expect(gl_user).to be_valid
expect(gl_user.external).to be_truthy
end
end
before { stub_saml_group_config(%w(Interns)) }
context 'are defined but the user does not belong there' do
it 'does not mark the user as external' do
saml_user.save
expect(gl_user).to be_valid
expect(gl_user.external).to be_falsey
end
end
context 'user was external, now should not be' do
it 'should make user internal' do
existing_user.update_attribute('external', true)
saml_user.save
expect(gl_user).to be_valid
expect(gl_user.external).to be_falsey
end
end
end
end end
describe 'no account exists on server' do describe 'no account exists on server' do
...@@ -68,6 +107,26 @@ describe Gitlab::Saml::User, lib: true do ...@@ -68,6 +107,26 @@ describe Gitlab::Saml::User, lib: true do
end end
end end
context 'external groups' do
context 'are defined' do
it 'marks the user as external' do
stub_saml_group_config(%w(Freelancers))
saml_user.save
expect(gl_user).to be_valid
expect(gl_user.external).to be_truthy
end
end
context 'are defined but the user does not belong there' do
it 'does not mark the user as external' do
stub_saml_group_config(%w(Interns))
saml_user.save
expect(gl_user).to be_valid
expect(gl_user.external).to be_falsey
end
end
end
context 'with auto_link_ldap_user disabled (default)' do context 'with auto_link_ldap_user disabled (default)' do
before { stub_omniauth_config({ auto_link_ldap_user: false, auto_link_saml_user: false, allow_single_sign_on: ['saml'] }) } before { stub_omniauth_config({ auto_link_ldap_user: false, auto_link_saml_user: false, allow_single_sign_on: ['saml'] }) }
include_examples 'to verify compliance with allow_single_sign_on' include_examples 'to verify compliance with allow_single_sign_on'
...@@ -76,12 +135,6 @@ describe Gitlab::Saml::User, lib: true do ...@@ -76,12 +135,6 @@ describe Gitlab::Saml::User, lib: true do
context 'with auto_link_ldap_user enabled' do context 'with auto_link_ldap_user enabled' do
before { stub_omniauth_config({ auto_link_ldap_user: true, auto_link_saml_user: false }) } before { stub_omniauth_config({ auto_link_ldap_user: true, auto_link_saml_user: false }) }
context 'and no LDAP provider defined' do
before { stub_ldap_config(providers: []) }
include_examples 'to verify compliance with allow_single_sign_on'
end
context 'and at least one LDAP provider is defined' do context 'and at least one LDAP provider is defined' do
before { stub_ldap_config(providers: %w(ldapmain)) } before { stub_ldap_config(providers: %w(ldapmain)) }
...@@ -89,19 +142,18 @@ describe Gitlab::Saml::User, lib: true do ...@@ -89,19 +142,18 @@ describe Gitlab::Saml::User, lib: true do
before do before do
allow(ldap_user).to receive(:uid) { uid } allow(ldap_user).to receive(:uid) { uid }
allow(ldap_user).to receive(:username) { uid } allow(ldap_user).to receive(:username) { uid }
allow(ldap_user).to receive(:email) { ['johndoe@example.com','john2@example.com'] } allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) }
allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' }
allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
end end
context 'and no account for the LDAP user' do context 'and no account for the LDAP user' do
it 'creates a user with dual LDAP and SAML identities' do it 'creates a user with dual LDAP and SAML identities' do
saml_user.save saml_user.save
expect(gl_user).to be_valid expect(gl_user).to be_valid
expect(gl_user.username).to eql uid expect(gl_user.username).to eql uid
expect(gl_user.email).to eql 'johndoe@example.com' expect(gl_user.email).to eql 'john@mail.com'
expect(gl_user.identities.length).to eql 2 expect(gl_user.identities.length).to eql 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
...@@ -111,13 +163,13 @@ describe Gitlab::Saml::User, lib: true do ...@@ -111,13 +163,13 @@ describe Gitlab::Saml::User, lib: true do
end end
context 'and LDAP user has an account already' do context 'and LDAP user has an account already' do
let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } let!(:existing_user) { create(:omniauth_user, email: 'john@mail.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') }
it "adds the omniauth identity to the LDAP account" do it 'adds the omniauth identity to the LDAP account' do
saml_user.save saml_user.save
expect(gl_user).to be_valid expect(gl_user).to be_valid
expect(gl_user.username).to eql 'john' expect(gl_user.username).to eql 'john'
expect(gl_user.email).to eql 'john@example.com' expect(gl_user.email).to eql 'john@mail.com'
expect(gl_user.identities.length).to eql 2 expect(gl_user.identities.length).to eql 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
...@@ -126,19 +178,13 @@ describe Gitlab::Saml::User, lib: true do ...@@ -126,19 +178,13 @@ describe Gitlab::Saml::User, lib: true do
end end
end end
end end
context 'and no corresponding LDAP person' do
before { allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) }
include_examples 'to verify compliance with allow_single_sign_on'
end
end end
end end
end end
describe 'blocking' do describe 'blocking' do
before { stub_omniauth_config({ allow_saml_sign_up: true, auto_link_saml_user: true }) } before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) }
context 'signup with SAML only' do context 'signup with SAML only' do
context 'dont block on create' do context 'dont block on create' do
...@@ -162,64 +208,6 @@ describe Gitlab::Saml::User, lib: true do ...@@ -162,64 +208,6 @@ describe Gitlab::Saml::User, lib: true do
end end
end end
context 'signup with linked omniauth and LDAP account' do
before do
stub_omniauth_config(auto_link_ldap_user: true)
allow(ldap_user).to receive(:uid) { uid }
allow(ldap_user).to receive(:username) { uid }
allow(ldap_user).to receive(:email) { ['johndoe@example.com','john2@example.com'] }
allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' }
allow(saml_user).to receive(:ldap_person).and_return(ldap_user)
end
context "and no account for the LDAP user" do
context 'dont block on create (LDAP)' do
before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) }
it do
saml_user.save
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
end
context 'block on create (LDAP)' do
before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) }
it do
saml_user.save
expect(gl_user).to be_valid
expect(gl_user).to be_blocked
end
end
end
context 'and LDAP user has an account already' do
let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') }
context 'dont block on create (LDAP)' do
before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) }
it do
saml_user.save
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
end
context 'block on create (LDAP)' do
before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) }
it do
saml_user.save
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
end
end
end
context 'sign-in' do context 'sign-in' do
before do before do
saml_user.save saml_user.save
...@@ -245,26 +233,6 @@ describe Gitlab::Saml::User, lib: true do ...@@ -245,26 +233,6 @@ describe Gitlab::Saml::User, lib: true do
expect(gl_user).not_to be_blocked expect(gl_user).not_to be_blocked
end end
end end
context 'dont block on create (LDAP)' do
before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) }
it do
saml_user.save
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
end
context 'block on create (LDAP)' do
before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) }
it do
saml_user.save
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
end
end end
end end
end end
......
...@@ -141,10 +141,12 @@ shared_examples 'a new user email' do ...@@ -141,10 +141,12 @@ shared_examples 'a new user email' do
end end
shared_examples 'it should have Gmail Actions links' do shared_examples 'it should have Gmail Actions links' do
it { is_expected.to have_body_text '<script type="application/ld+json">' }
it { is_expected.to have_body_text /ViewAction/ } it { is_expected.to have_body_text /ViewAction/ }
end end
shared_examples 'it should not have Gmail Actions links' do shared_examples 'it should not have Gmail Actions links' do
it { is_expected.to_not have_body_text '<script type="application/ld+json">' }
it { is_expected.to_not have_body_text /ViewAction/ } it { is_expected.to_not have_body_text /ViewAction/ }
end end
......
...@@ -6,18 +6,38 @@ describe BuildsEmailService do ...@@ -6,18 +6,38 @@ describe BuildsEmailService do
let(:service) { BuildsEmailService.new } let(:service) { BuildsEmailService.new }
describe :execute do describe :execute do
it "sends email" do it 'sends email' do
service.recipients = 'test@gitlab.com' service.recipients = 'test@gitlab.com'
data[:build_status] = 'failed' data[:build_status] = 'failed'
expect(BuildEmailWorker).to receive(:perform_async) expect(BuildEmailWorker).to receive(:perform_async)
service.execute(data) service.execute(data)
end end
it "does not sends email with failed build and allowed_failure on" do it 'does not send email with succeeded build and notify_only_broken_builds on' do
expect(service).to receive(:notify_only_broken_builds).and_return(true)
data[:build_status] = 'success'
expect(BuildEmailWorker).not_to receive(:perform_async)
service.execute(data)
end
it 'does not send email with failed build and build_allow_failure is true' do
data[:build_status] = 'failed' data[:build_status] = 'failed'
data[:build_allow_failure] = true data[:build_allow_failure] = true
expect(BuildEmailWorker).not_to receive(:perform_async) expect(BuildEmailWorker).not_to receive(:perform_async)
service.execute(data) service.execute(data)
end end
it 'does not send email with unknown build status' do
data[:build_status] = 'foo'
expect(BuildEmailWorker).not_to receive(:perform_async)
service.execute(data)
end
it 'does not send email when recipients list is empty' do
service.recipients = ' ,, '
data[:build_status] = 'failed'
expect(BuildEmailWorker).not_to receive(:perform_async)
service.execute(data)
end
end end
end end
...@@ -4,6 +4,7 @@ describe API::API, api: true do ...@@ -4,6 +4,7 @@ describe API::API, api: true do
include ApiHelpers include ApiHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let!(:project) { create(:project, namespace: user.namespace ) } let!(:project) { create(:project, namespace: user.namespace ) }
let!(:closed_milestone) { create(:closed_milestone, project: project) }
let!(:milestone) { create(:milestone, project: project) } let!(:milestone) { create(:milestone, project: project) }
before { project.team << [user, :developer] } before { project.team << [user, :developer] }
...@@ -20,6 +21,24 @@ describe API::API, api: true do ...@@ -20,6 +21,24 @@ describe API::API, api: true do
get api("/projects/#{project.id}/milestones") get api("/projects/#{project.id}/milestones")
expect(response.status).to eq(401) expect(response.status).to eq(401)
end end
it 'returns an array of active milestones' do
get api("/projects/#{project.id}/milestones?state=active", user)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(milestone.id)
end
it 'returns an array of closed milestones' do
get api("/projects/#{project.id}/milestones?state=closed", user)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(closed_milestone.id)
end
end end
describe 'GET /projects/:id/milestones/:milestone_id' do describe 'GET /projects/:id/milestones/:milestone_id' do
......
...@@ -164,21 +164,37 @@ describe GitPushService, services: true do ...@@ -164,21 +164,37 @@ describe GitPushService, services: true do
end end
context "after push" do context "after push" do
before do def execute
@service = execute_service(project, user, @oldrev, @newrev, ref) execute_service(project, user, @oldrev, @newrev, ref)
end end
context "to master" do context "to master" do
let(:ref) { @ref } let(:ref) { @ref }
it { expect(@service.update_main_language).to eq(true) } context 'when main_language is nil' do
it { expect(project.main_language).to eq("Ruby") } it 'obtains the language from the repository' do
expect(project.repository).to receive(:main_language)
execute
end
it 'sets the project main language' do
execute
expect(project.main_language).to eq("Ruby")
end
end
context 'when main_language is already set' do
it 'does not check the repository' do
execute # do an initial run to simulate lang being preset
expect(project.repository).not_to receive(:main_language)
execute
end
end
end end
context "to other branch" do context "to other branch" do
let(:ref) { 'refs/heads/feature/branch' } let(:ref) { 'refs/heads/feature/branch' }
it { expect(@service.update_main_language).to eq(nil) }
it { expect(project.main_language).to eq(nil) } it { expect(project.main_language).to eq(nil) }
end end
end end
......
...@@ -72,14 +72,15 @@ module MarkdownMatchers ...@@ -72,14 +72,15 @@ module MarkdownMatchers
have_css("img[src$='#{src}']") have_css("img[src$='#{src}']")
end end
prefix = '/namespace1/gitlabhq/wikis'
set_default_markdown_messages set_default_markdown_messages
match do |actual| match do |actual|
expect(actual).to have_link('linked-resource', href: 'linked-resource') expect(actual).to have_link('linked-resource', href: "#{prefix}/linked-resource")
expect(actual).to have_link('link-text', href: 'linked-resource') expect(actual).to have_link('link-text', href: "#{prefix}/linked-resource")
expect(actual).to have_link('http://example.com', href: 'http://example.com') expect(actual).to have_link('http://example.com', href: 'http://example.com')
expect(actual).to have_link('link-text', href: 'http://example.com/pdfs/gollum.pdf') expect(actual).to have_link('link-text', href: 'http://example.com/pdfs/gollum.pdf')
expect(actual).to have_image('/gitlabhq/wikis/images/example.jpg') expect(actual).to have_image("#{prefix}/images/example.jpg")
expect(actual).to have_image('http://example.com/images/example.jpg') expect(actual).to have_image('http://example.com/images/example.jpg')
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment