mirror of
https://github.com/wahyd4/gitlabhq.git
synced 2026-08-16 08:06:08 +10:00
Merge branch 'master' into 8-7-stable
This commit is contained in:
+1
-78
@@ -2,7 +2,6 @@ image: "ruby:2.1"
|
||||
|
||||
services:
|
||||
- mysql:latest
|
||||
- postgres:latest
|
||||
- redis:latest
|
||||
|
||||
cache:
|
||||
@@ -35,134 +34,86 @@ spec:feature:
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
spec:api:
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
spec:models:
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
spec:lib:
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
spec:services:
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
spec:other:
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
spinach:project:half:
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
spinach:project:rest:
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
spinach:other:
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
teaspoon:
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec teaspoon
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
rubocop:
|
||||
stage: test
|
||||
script:
|
||||
- bundle exec rubocop
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
scss-lint:
|
||||
stage: test
|
||||
script:
|
||||
- bundle exec rake scss_lint
|
||||
tags:
|
||||
- ruby
|
||||
|
||||
brakeman:
|
||||
stage: test
|
||||
script:
|
||||
- bundle exec rake brakeman
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
flog:
|
||||
stage: test
|
||||
script:
|
||||
- bundle exec rake flog
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
flay:
|
||||
stage: test
|
||||
script:
|
||||
- bundle exec rake flay
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
bundler:audit:
|
||||
stage: test
|
||||
only:
|
||||
- master
|
||||
script:
|
||||
- "bundle exec bundle-audit update"
|
||||
- "bundle exec bundle-audit check --ignore OSVDB-115941"
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
- "bundle exec bundle-audit check --update --ignore OSVDB-115941"
|
||||
|
||||
# Ruby 2.2 jobs
|
||||
|
||||
@@ -178,9 +129,6 @@ spec:feature:ruby22:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
spec:api:ruby22:
|
||||
stage: test
|
||||
@@ -193,9 +141,6 @@ spec:api:ruby22:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
spec:models:ruby22:
|
||||
stage: test
|
||||
@@ -208,9 +153,6 @@ spec:models:ruby22:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
spec:lib:ruby22:
|
||||
stage: test
|
||||
@@ -223,9 +165,6 @@ spec:lib:ruby22:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
spec:services:ruby22:
|
||||
stage: test
|
||||
@@ -238,9 +177,6 @@ spec:services:ruby22:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
spec:other:ruby22:
|
||||
stage: test
|
||||
@@ -253,9 +189,6 @@ spec:other:ruby22:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
spinach:project:half:ruby22:
|
||||
stage: test
|
||||
@@ -269,9 +202,6 @@ spinach:project:half:ruby22:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
spinach:project:rest:ruby22:
|
||||
stage: test
|
||||
@@ -285,9 +215,6 @@ spinach:project:rest:ruby22:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
spinach:other:ruby22:
|
||||
stage: test
|
||||
@@ -301,10 +228,6 @@ spinach:other:ruby22:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
tags:
|
||||
- ruby
|
||||
- mysql
|
||||
|
||||
|
||||
notify:slack:
|
||||
stage: notifications
|
||||
|
||||
@@ -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)
|
||||
- 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)
|
||||
- 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
|
||||
- Make HTTP(s) label consistent on clone bar (Stan Hu)
|
||||
- Expose label description in API (Mariusz Jachimowicz)
|
||||
- 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
|
||||
- Allow SAML to handle external users based on user's information !3530
|
||||
- Add endpoints to archive or unarchive a project !3372
|
||||
- Add links to CI setup documentation from project settings and builds pages
|
||||
- Handle nil descriptions in Slack issue messages (Stan Hu)
|
||||
- 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 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
|
||||
- Gracefully handle notes on deleted commits in merge requests (Stan Hu)
|
||||
@@ -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)
|
||||
- Remove "Congratulations!" tweet button on newly-created project. (Connor Shea)
|
||||
- Improved UX of the navigation sidebar
|
||||
- Fix admin/projects when using visibility levels on search (PotHix)
|
||||
- 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
|
||||
- Don't attempt to fetch any tags from a forked repo (Stan Hu)
|
||||
@@ -140,6 +152,9 @@ v 8.6.0
|
||||
- Trigger a todo for mentions on commits page
|
||||
- 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
|
||||
- Bump Git version requirement to 2.7.4
|
||||
|
||||
@@ -281,6 +296,12 @@ v 8.5.0
|
||||
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
|
||||
- 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
|
||||
- No CE-specific changes
|
||||
|
||||
@@ -394,6 +415,12 @@ v 8.4.0
|
||||
- Add IP check against DNSBLs at account sign-up
|
||||
- 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
|
||||
- Use gitlab-workhorse 0.5.4 (fixes API routing bug)
|
||||
|
||||
|
||||
@@ -290,7 +290,7 @@ group :development, :test do
|
||||
gem 'rubocop', '~> 0.38.0', require: false
|
||||
gem 'scss_lint', '~> 0.47.0', 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 'flay', require: false
|
||||
gem 'bundler-audit', require: false
|
||||
|
||||
+6
-16
@@ -99,7 +99,7 @@ GEM
|
||||
bullet (5.0.0)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.9.0)
|
||||
bundler-audit (0.4.0)
|
||||
bundler-audit (0.5.0)
|
||||
bundler (~> 1.2)
|
||||
thor (~> 0.18)
|
||||
byebug (8.2.1)
|
||||
@@ -136,10 +136,9 @@ GEM
|
||||
colorize (0.7.7)
|
||||
concurrent-ruby (1.0.0)
|
||||
connection_pool (2.2.0)
|
||||
coveralls (0.8.9)
|
||||
coveralls (0.8.13)
|
||||
json (~> 1.8)
|
||||
rest-client (>= 1.6.8, < 2)
|
||||
simplecov (~> 0.10.0)
|
||||
simplecov (~> 0.11.0)
|
||||
term-ansicolor (~> 1.3)
|
||||
thor (~> 0.19.1)
|
||||
tins (~> 1.6.0)
|
||||
@@ -176,8 +175,6 @@ GEM
|
||||
diff-lcs (1.2.5)
|
||||
diffy (3.0.7)
|
||||
docile (1.1.5)
|
||||
domain_name (0.5.25)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (2.2.2)
|
||||
railties (>= 3.2)
|
||||
dropzonejs-rails (0.7.2)
|
||||
@@ -421,8 +418,6 @@ GEM
|
||||
nokogiri (~> 1.6.0)
|
||||
ruby_parser (~> 3.5)
|
||||
htmlentities (4.3.4)
|
||||
http-cookie (1.0.2)
|
||||
domain_name (~> 0.5)
|
||||
http_parser.rb (0.5.3)
|
||||
httparty (0.13.7)
|
||||
json (~> 1.8)
|
||||
@@ -480,7 +475,6 @@ GEM
|
||||
nested_form (0.3.2)
|
||||
net-ldap (0.12.1)
|
||||
net-ssh (3.0.1)
|
||||
netrc (0.11.0)
|
||||
newrelic_rpm (3.14.1.311)
|
||||
nokogiri (1.6.7.2)
|
||||
mini_portile2 (~> 2.0.0.rc2)
|
||||
@@ -657,10 +651,6 @@ GEM
|
||||
listen (~> 3.0)
|
||||
responders (2.1.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)
|
||||
rotp (2.1.1)
|
||||
rouge (1.10.1)
|
||||
@@ -754,7 +744,7 @@ GEM
|
||||
rufus-scheduler (>= 2.0.24)
|
||||
sidekiq (>= 4.0.0)
|
||||
simple_oauth (0.1.9)
|
||||
simplecov (0.10.0)
|
||||
simplecov (0.11.2)
|
||||
docile (~> 1.1.0)
|
||||
json (~> 1.8)
|
||||
simplecov-html (~> 0.10.0)
|
||||
@@ -845,7 +835,7 @@ GEM
|
||||
underscore-rails (1.8.3)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.1)
|
||||
unf_ext (0.0.7.2)
|
||||
unicode-display_width (1.0.2)
|
||||
unicorn (4.9.0)
|
||||
kgio (~> 2.6)
|
||||
@@ -1032,7 +1022,7 @@ DEPENDENCIES
|
||||
shoulda-matchers (~> 2.8.0)
|
||||
sidekiq (~> 4.0)
|
||||
sidekiq-cron (~> 0.4.0)
|
||||
simplecov (~> 0.10.0)
|
||||
simplecov (~> 0.11.0)
|
||||
sinatra (~> 1.4.4)
|
||||
six (~> 0.2.0)
|
||||
slack-notifier (~> 1.2.0)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class GitLabDropdownFilter
|
||||
BLUR_KEYCODES = [27, 40]
|
||||
ARROW_KEY_CODES = [38, 40]
|
||||
HAS_VALUE_CLASS = "has-value"
|
||||
|
||||
constructor: (@input, @options) ->
|
||||
@@ -22,19 +23,23 @@ class GitLabDropdownFilter
|
||||
# Key events
|
||||
timeout = ""
|
||||
@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
|
||||
$inputContainer.addClass HAS_VALUE_CLASS
|
||||
else if @input.val() is "" and $inputContainer.hasClass 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
|
||||
@options.enterCallback()
|
||||
return
|
||||
|
||||
clearTimeout timeout
|
||||
timeout = setTimeout =>
|
||||
blur_field = @shouldBlur e.keyCode
|
||||
blur_field = @shouldBlur keyCode
|
||||
search_text = @input.val()
|
||||
|
||||
if blur_field and @filterInputBlur
|
||||
@@ -96,6 +101,7 @@ class GitLabDropdown
|
||||
LOADING_CLASS = "is-loading"
|
||||
PAGE_TWO_CLASS = "is-page-two"
|
||||
ACTIVE_CLASS = "is-active"
|
||||
currentIndex = -1
|
||||
|
||||
FILTER_INPUT = '.dropdown-input .dropdown-input-field'
|
||||
|
||||
@@ -145,11 +151,11 @@ class GitLabDropdown
|
||||
data: =>
|
||||
return @fullData
|
||||
callback: (data) =>
|
||||
currentIndex = -1
|
||||
@parseData data
|
||||
@highlightRow 1
|
||||
enterCallback: =>
|
||||
if @enterCallback
|
||||
@selectFirstRow()
|
||||
@selectRowAtIndex 0
|
||||
|
||||
# Event listeners
|
||||
|
||||
@@ -171,10 +177,11 @@ class GitLabDropdown
|
||||
selector = ".dropdown-page-one .dropdown-content a"
|
||||
|
||||
@dropdown.on "click", selector, (e) ->
|
||||
selected = self.rowClicked $(@)
|
||||
$el = $(@)
|
||||
selected = self.rowClicked $el
|
||||
|
||||
if self.options.clicked
|
||||
self.options.clicked(selected)
|
||||
self.options.clicked(selected, $el, e)
|
||||
|
||||
# Finds an element inside wrapper element
|
||||
getElement: (selector) ->
|
||||
@@ -218,6 +225,8 @@ class GitLabDropdown
|
||||
return true
|
||||
|
||||
opened: =>
|
||||
@addArrowKeyEvent()
|
||||
|
||||
contentHtml = $('.dropdown-content', @dropdown).html()
|
||||
if @remote && contentHtml is ""
|
||||
@remote.execute()
|
||||
@@ -228,6 +237,7 @@ class GitLabDropdown
|
||||
@dropdown.trigger('shown.gl.dropdown')
|
||||
|
||||
hidden: (e) =>
|
||||
@removeArrayKeyEvent()
|
||||
if @options.filterable
|
||||
@dropdown
|
||||
.find(".dropdown-input-field")
|
||||
@@ -307,11 +317,11 @@ class GitLabDropdown
|
||||
if @highlight
|
||||
text = @highlightTextMatches(text, @filterInput.val())
|
||||
|
||||
html = "<li>"
|
||||
html += "<a href='#{url}' class='#{cssClass}'>"
|
||||
html += text
|
||||
html += "</a>"
|
||||
html += "</li>"
|
||||
html = "<li>
|
||||
<a href='#{url}' class='#{cssClass}'>
|
||||
#{text}
|
||||
</a>
|
||||
</li>"
|
||||
|
||||
return html
|
||||
|
||||
@@ -322,11 +332,11 @@ class GitLabDropdown
|
||||
).join('')
|
||||
|
||||
noResults: ->
|
||||
html = "<li>"
|
||||
html += "<a class='dropdown-menu-empty-link is-focused'>"
|
||||
html += "No matching results."
|
||||
html += "</a>"
|
||||
html += "</li>"
|
||||
html = "<li class='dropdown-menu-empty-link'>
|
||||
<a href='#' class='is-focused'>
|
||||
No matching results.
|
||||
</a>
|
||||
</li>"
|
||||
|
||||
highlightRow: (index) ->
|
||||
if @filterInput.val() isnt ""
|
||||
@@ -351,6 +361,8 @@ class GitLabDropdown
|
||||
# Toggle the dropdown label
|
||||
if @options.toggleLabel
|
||||
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel
|
||||
else
|
||||
selectedObject
|
||||
else
|
||||
if !value?
|
||||
field.remove()
|
||||
@@ -366,7 +378,7 @@ class GitLabDropdown
|
||||
if @options.toggleLabel
|
||||
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject)
|
||||
if value?
|
||||
if !field.length
|
||||
if !field.length and fieldName
|
||||
# Create hidden input for form
|
||||
input = "<input type='hidden' name='#{fieldName}' value='#{value}' />"
|
||||
if @options.inputId?
|
||||
@@ -378,16 +390,81 @@ class GitLabDropdown
|
||||
|
||||
return selectedObject
|
||||
|
||||
selectFirstRow: ->
|
||||
selector = '.dropdown-content li:first-child a'
|
||||
selectRowAtIndex: (index) ->
|
||||
selector = ".dropdown-content li:not(.divider):eq(#{index}) a"
|
||||
|
||||
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
|
||||
$(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) ->
|
||||
return @.each ->
|
||||
if (!$.data @, 'glDropdown')
|
||||
$.data(@, 'glDropdown', new GitLabDropdown @, opts)
|
||||
|
||||
|
||||
@@ -6,25 +6,10 @@ class @Issue
|
||||
constructor: ->
|
||||
# Prevent duplicate event bindings
|
||||
@disableTaskList()
|
||||
@fixAffixScroll()
|
||||
if $('a.btn-close').length
|
||||
@initTaskList()
|
||||
@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: ->
|
||||
$('.detail-page-description .js-task-list-container').taskList('enable')
|
||||
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
|
||||
@@ -49,7 +34,7 @@ class @Issue
|
||||
issueStatus = if isClose then 'close' else 'open'
|
||||
new Flash(issueFailMessage, 'alert')
|
||||
success: (data, textStatus, jqXHR) ->
|
||||
if data.saved
|
||||
if 'id' of data
|
||||
$(document).trigger('issuable:change');
|
||||
if isClose
|
||||
$('a.btn-close').addClass('hidden')
|
||||
|
||||
@@ -26,6 +26,20 @@
|
||||
|
||||
$(".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
|
||||
initSearch: ->
|
||||
@timer = null
|
||||
@@ -54,6 +68,7 @@
|
||||
# Change url so if user reload a page - search results are saved
|
||||
history.replaceState {page: issuesUrl}, document.title, issuesUrl
|
||||
Issues.reload()
|
||||
Issues.updateStateFilters()
|
||||
dataType: "json"
|
||||
|
||||
checkChanged: ->
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
((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
|
||||
this.$('.show-all-commits').on 'click', =>
|
||||
this.showAllCommits()
|
||||
|
||||
@fixAffixScroll();
|
||||
|
||||
@initTabs()
|
||||
|
||||
# Prevent duplicate event bindings
|
||||
@@ -30,20 +28,6 @@ class @MergeRequest
|
||||
$: (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: ->
|
||||
if @opts.action != 'new'
|
||||
# `MergeRequests#new` has no tab-persisting or lazy-loading behavior
|
||||
|
||||
@@ -15,6 +15,8 @@ class @MergeRequestWidget
|
||||
@pollCIStatus()
|
||||
notifyPermissions()
|
||||
|
||||
setOpts: (@opts) ->
|
||||
|
||||
mergeInProgress: (deleteSourceBranch = false)->
|
||||
$.ajax
|
||||
type: 'GET'
|
||||
@@ -48,7 +50,7 @@ class @MergeRequestWidget
|
||||
@getCIStatus(true)
|
||||
|
||||
@readyForCICheck = false
|
||||
), 5000
|
||||
), 10000
|
||||
|
||||
getCIStatus: (showNotification) ->
|
||||
_this = @
|
||||
@@ -61,6 +63,10 @@ class @MergeRequestWidget
|
||||
@firstCICheck = false
|
||||
@opts.ci_status = data.status
|
||||
|
||||
if @opts.ci_status is ''
|
||||
@opts.ci_status = data.status
|
||||
return
|
||||
|
||||
if data.status isnt @opts.ci_status
|
||||
@showCIStatus data.status
|
||||
if data.coverage
|
||||
|
||||
@@ -251,13 +251,11 @@ class @Notes
|
||||
Sets some hidden fields in the form.
|
||||
###
|
||||
setupMainTargetNoteForm: ->
|
||||
|
||||
# find the form
|
||||
form = $(".js-new-note-form")
|
||||
|
||||
# insert the form after the button
|
||||
form.clone().replaceAll $(".js-main-target-form")
|
||||
form = form.prev("form")
|
||||
# Set a global clone of the form for later cloning
|
||||
@formClone = form.clone()
|
||||
|
||||
# show the form
|
||||
@setupNoteForm(form)
|
||||
@@ -266,9 +264,7 @@ class @Notes
|
||||
form.removeClass "js-new-note-form"
|
||||
form.addClass "js-main-target-form"
|
||||
|
||||
# remove unnecessary fields and buttons
|
||||
form.find("#note_line_code").remove()
|
||||
form.find(".js-close-discussion-note-form").remove()
|
||||
|
||||
###
|
||||
General note form setup.
|
||||
@@ -297,7 +293,14 @@ class @Notes
|
||||
else
|
||||
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)
|
||||
|
||||
new Autosave textarea, [
|
||||
"Note"
|
||||
form.find("#note_commit_id").val()
|
||||
@@ -307,7 +310,6 @@ class @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()
|
||||
new DropzoneInput(form)
|
||||
form.show()
|
||||
@@ -455,15 +457,15 @@ class @Notes
|
||||
Shows the note form below the notes.
|
||||
###
|
||||
replyToDiscussionNote: (e) =>
|
||||
form = $(".js-new-note-form")
|
||||
form = @formClone.clone()
|
||||
replyLink = $(e.target).closest(".js-discussion-reply-button")
|
||||
replyLink.hide()
|
||||
|
||||
# insert the form after the button
|
||||
form.clone().insertAfter replyLink
|
||||
replyLink.after form
|
||||
|
||||
# show the form
|
||||
@setupDiscussionNoteForm(replyLink, replyLink.next("form"))
|
||||
@setupDiscussionNoteForm(replyLink, form)
|
||||
|
||||
###
|
||||
Shows the diff or discussion form and does some setup on it.
|
||||
@@ -488,7 +490,9 @@ class @Notes
|
||||
.text(form.find('.js-close-discussion-note-form').data('cancel-text'))
|
||||
@setupNoteForm form
|
||||
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.
|
||||
@@ -498,9 +502,8 @@ class @Notes
|
||||
###
|
||||
addDiffNote: (e) =>
|
||||
e.preventDefault()
|
||||
link = e.currentTarget
|
||||
form = $(".js-new-note-form")
|
||||
row = $(link).closest("tr")
|
||||
$link = $(e.currentTarget)
|
||||
row = $link.closest("tr")
|
||||
nextRow = row.next()
|
||||
hasNotes = nextRow.is(".notes_holder")
|
||||
addForm = false
|
||||
@@ -509,7 +512,7 @@ class @Notes
|
||||
|
||||
# In parallel view, look inside the correct left/right pane
|
||||
if @isParallelView()
|
||||
lineType = $(link).data("lineType")
|
||||
lineType = $link.data("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>"
|
||||
|
||||
@@ -531,11 +534,11 @@ class @Notes
|
||||
addForm = true
|
||||
|
||||
if addForm
|
||||
newForm = form.clone()
|
||||
newForm = @formClone.clone()
|
||||
newForm.appendTo row.next().find(targetContent)
|
||||
|
||||
# show the form
|
||||
@setupDiscussionNoteForm $(link), newForm
|
||||
@setupDiscussionNoteForm $link, newForm
|
||||
|
||||
###
|
||||
Called in response to "cancel" on a diff note form.
|
||||
@@ -560,7 +563,6 @@ class @Notes
|
||||
|
||||
cancelDiscussionForm: (e) =>
|
||||
e.preventDefault()
|
||||
form = $(".js-new-note-form")
|
||||
form = $(e.target).closest(".js-discussion-note-form")
|
||||
@removeDiscussionNoteForm(form)
|
||||
|
||||
|
||||
@@ -62,6 +62,8 @@ class @SearchAutocomplete
|
||||
search:
|
||||
fields: ['text']
|
||||
data: @getData.bind(@)
|
||||
selectable: true
|
||||
clicked: @onClick.bind(@)
|
||||
|
||||
getData: (term, callback) ->
|
||||
_this = @
|
||||
@@ -102,6 +104,8 @@ class @SearchAutocomplete
|
||||
lastCategory = suggestion.category
|
||||
|
||||
data.push
|
||||
id: "#{suggestion.category.toLowerCase()}-#{suggestion.id}"
|
||||
category: suggestion.category
|
||||
text: suggestion.label
|
||||
url: suggestion.url
|
||||
|
||||
@@ -133,12 +137,19 @@ class @SearchAutocomplete
|
||||
}
|
||||
|
||||
bindEvents: ->
|
||||
$(document).on 'click', @onDocumentClick
|
||||
@searchInput.on 'keydown', @onSearchInputKeyDown
|
||||
@searchInput.on 'keyup', @onSearchInputKeyUp
|
||||
@searchInput.on 'click', @onSearchInputClick
|
||||
@searchInput.on 'focus', @onSearchInputFocus
|
||||
@searchInput.on 'blur', @onSearchInputBlur
|
||||
@clearInput.on 'click', @onRemoveLocationClick
|
||||
@clearInput.on 'click', @onClearInputClick
|
||||
|
||||
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: ->
|
||||
# No need to enable anything if user is not logged in
|
||||
@@ -181,6 +192,8 @@ class @SearchAutocomplete
|
||||
# We should display the menu only when input is not empty
|
||||
@enableAutocomplete()
|
||||
|
||||
@wrap.toggleClass 'has-value', !!e.target.value
|
||||
|
||||
# Avoid falsy value to be returned
|
||||
return
|
||||
|
||||
@@ -189,27 +202,20 @@ class @SearchAutocomplete
|
||||
e.stopImmediatePropagation()
|
||||
|
||||
onSearchInputFocus: =>
|
||||
@isFocused = true
|
||||
@wrap.addClass('search-active')
|
||||
|
||||
onRemoveLocationClick: (e) =>
|
||||
onClearInputClick: (e) =>
|
||||
e.preventDefault()
|
||||
@removeLocationBadge()
|
||||
@searchInput.val('').focus()
|
||||
@skipBlurEvent = true
|
||||
|
||||
onSearchInputBlur: (e) =>
|
||||
@skipBlurEvent = false
|
||||
@isFocused = false
|
||||
@wrap.removeClass('search-active')
|
||||
|
||||
# We should wait to make sure we are not clearing the input instead
|
||||
setTimeout( =>
|
||||
return if @skipBlurEvent
|
||||
|
||||
@wrap.removeClass('search-active')
|
||||
|
||||
# If input is blank then restore state
|
||||
if @searchInput.val() is ''
|
||||
@restoreOriginalState()
|
||||
, 150)
|
||||
# If input is blank then restore state
|
||||
if @searchInput.val() is ''
|
||||
@restoreOriginalState()
|
||||
|
||||
addLocationBadge: (item) ->
|
||||
category = if item.category? then "#{item.category}: " else ''
|
||||
@@ -268,3 +274,23 @@ class @SearchAutocomplete
|
||||
<li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li>
|
||||
</ul>"
|
||||
@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
|
||||
$(e.currentTarget).trigger('zen_mode:leave')
|
||||
|
||||
$(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) =>
|
||||
@exit()
|
||||
|
||||
|
||||
@@ -125,13 +125,6 @@ p.time {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
// Fixes alignment on notes.
|
||||
.new_note {
|
||||
label {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix issue with notes & lists creating a bunch of bottom borders.
|
||||
li.note {
|
||||
img { max-width: 100% }
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
padding: 10px $gl-padding;
|
||||
word-wrap: break-word;
|
||||
|
||||
.file-actions {
|
||||
float: right;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
.div-dropzone-wrapper {
|
||||
.div-dropzone {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: -5px;
|
||||
|
||||
.div-dropzone-focus {
|
||||
border-color: #66afe9 !important;
|
||||
@@ -25,12 +23,10 @@
|
||||
|
||||
.div-dropzone-spinner {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 100%;
|
||||
margin-top: -1.1em;
|
||||
margin-left: -1.1em;
|
||||
bottom: 10px;
|
||||
right: 5px;
|
||||
opacity: 0;
|
||||
font-size: 30px;
|
||||
font-size: 20px;
|
||||
transition: opacity 200ms ease-in-out;
|
||||
}
|
||||
|
||||
@@ -65,17 +61,30 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.md-header {
|
||||
.nav-links {
|
||||
.active {
|
||||
a {
|
||||
border-bottom-color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
padding-top: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.referenced-users {
|
||||
color: #4c4e54;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.md-preview-holder {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
min-height: 169px;
|
||||
padding: 5px;
|
||||
box-shadow: none;
|
||||
min-height: 167px;
|
||||
padding: 10px 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.markdown-area {
|
||||
|
||||
@@ -250,7 +250,7 @@ a > code {
|
||||
* Textareas intended for GFM
|
||||
*
|
||||
*/
|
||||
textarea.js-gfm-input {
|
||||
.js-gfm-input {
|
||||
font-family: $monospace_font;
|
||||
color: $gl-text-color;
|
||||
}
|
||||
|
||||
@@ -104,9 +104,9 @@ $orange-light: rgba(252, 109, 38, 0.80);
|
||||
$orange-normal: #e75e40;
|
||||
$orange-dark: #ce5237;
|
||||
|
||||
$red-light: #f06559;
|
||||
$red-normal: #e52c5a;
|
||||
$red-dark: #d22852;
|
||||
$red-light: #e52c5a;
|
||||
$red-normal: #d22852;
|
||||
$red-dark: darken($red-normal, 5%);
|
||||
|
||||
$border-white-light: #f1f2f4;
|
||||
$border-white-normal: #d6dae2;
|
||||
@@ -128,9 +128,9 @@ $border-orange-light: #fc6d26;
|
||||
$border-orange-normal: #ce5237;
|
||||
$border-orange-dark: #c14e35;
|
||||
|
||||
$border-red-light: #f24f41;
|
||||
$border-red-normal: #d22852;
|
||||
$border-red-dark: #ca264f;
|
||||
$border-red-light: #d22852;
|
||||
$border-red-normal: #ca264f;
|
||||
$border-red-dark: darken($border-red-normal, 5%);
|
||||
|
||||
$help-well-bg: #fafafa;
|
||||
$help-well-border: #e5e5e5;
|
||||
@@ -201,14 +201,14 @@ $award-emoji-new-btn-icon-color: #dcdcdc;
|
||||
/*
|
||||
* 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-width: $dropdown-width;
|
||||
$search-input-width: 244px;
|
||||
$location-badge-color: #aaa;
|
||||
$location-badge-bg: $gray-normal;
|
||||
$location-badge-active-bg: #4f91f8;
|
||||
$location-icon-color: #e7e9ed;
|
||||
$location-active-color: $gl-text-color;
|
||||
$location-active-bg: $search-input-border-color;
|
||||
$location-icon-active-color: #807e7e;
|
||||
|
||||
/*
|
||||
* Notes
|
||||
@@ -217,3 +217,9 @@ $notes-light-color: #8e8e8e;
|
||||
$notes-action-color: #c3c3c3;
|
||||
$notes-role-color: #8e8e8e;
|
||||
$notes-role-border-color: #e4e4e4;
|
||||
|
||||
$note-disabled-comment-color: #b2b2b2;
|
||||
$note-form-border-color: #e5e5e5;
|
||||
$note-toolbar-color: #959494;
|
||||
|
||||
$zen-control-hover-color: #111;
|
||||
|
||||
@@ -1,61 +1,62 @@
|
||||
.zennable {
|
||||
a.js-zen-enter {
|
||||
color: $gl-gray;
|
||||
position: absolute;
|
||||
.zen-backdrop {
|
||||
&.fullscreen {
|
||||
background-color: white;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 4px;
|
||||
line-height: 56px;
|
||||
}
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1031;
|
||||
|
||||
a.js-zen-leave {
|
||||
display: none;
|
||||
color: $gl-text-color;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
padding: 5px;
|
||||
font-size: 36px;
|
||||
|
||||
&:hover {
|
||||
color: #111;
|
||||
textarea {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
color: #000;
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
padding: 30px;
|
||||
display: block;
|
||||
outline: none;
|
||||
resize: none;
|
||||
height: 100vh;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.zen-backdrop {
|
||||
&.fullscreen {
|
||||
background-color: white;
|
||||
position: fixed;
|
||||
.zen-control-leave {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1031;
|
||||
|
||||
textarea {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
color: #000;
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
padding: 30px;
|
||||
display: block;
|
||||
outline: none;
|
||||
resize: none;
|
||||
height: 100vh;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
a.js-zen-enter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
a.js-zen-leave {
|
||||
display: block;
|
||||
position: absolute;
|
||||
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 @@
|
||||
.description {
|
||||
margin-top: 6px;
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
p {
|
||||
overflow-x: auto;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,10 +59,15 @@
|
||||
border-collapse: separate;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
.line_holder td {
|
||||
line-height: $code_line_height;
|
||||
font-size: $code_font_size;
|
||||
}
|
||||
|
||||
td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
tr.line_holder.parallel {
|
||||
|
||||
@@ -195,42 +195,6 @@
|
||||
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 {
|
||||
.table-holder {
|
||||
overflow-x: scroll;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
/**
|
||||
* Note Form
|
||||
*/
|
||||
|
||||
.comment-btn {
|
||||
@extend .btn-create;
|
||||
}
|
||||
.reply-btn {
|
||||
@extend .btn-primary;
|
||||
margin: 10px $gl-padding;
|
||||
@@ -17,16 +13,17 @@
|
||||
}
|
||||
.diff-file,
|
||||
.discussion {
|
||||
.new_note {
|
||||
.new-note {
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
.new_note {
|
||||
|
||||
.new-note {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.new_note, .note-edit-form {
|
||||
.new-note, .note-edit-form {
|
||||
.note-form-actions {
|
||||
margin-top: $gl-padding;
|
||||
}
|
||||
@@ -40,19 +37,16 @@
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.note_text {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.comment-hints {
|
||||
margin-top: -12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* loading indicator */
|
||||
.notes-busy {
|
||||
margin: 18px;
|
||||
.note-textarea {
|
||||
padding: 10px 0;
|
||||
font-family: $regular_font;
|
||||
border: 0;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.note-image-attach {
|
||||
@@ -62,38 +56,29 @@
|
||||
}
|
||||
|
||||
.common-note-form {
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
padding: $gl-padding;
|
||||
margin-left: -$gl-padding;
|
||||
margin-right: -$gl-padding;
|
||||
margin-bottom: -$gl-padding;
|
||||
}
|
||||
.md-area {
|
||||
padding: $gl-padding-top $gl-padding;
|
||||
border: 1px solid $note-form-border-color;
|
||||
border-radius: $border-radius-base;
|
||||
|
||||
.note-form-actions {
|
||||
.note-form-option {
|
||||
margin-top: 8px;
|
||||
margin-left: 30px;
|
||||
@extend .pull-left;
|
||||
}
|
||||
&.is-focused {
|
||||
border-color: $focus-border-color;
|
||||
box-shadow: 0 0 2px rgba(#000, .2),
|
||||
0 0 4px rgba($focus-border-color, .4);
|
||||
|
||||
.js-notify-commit-author {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.write-preview-btn {
|
||||
// makes the "absolute" position for links relative to this
|
||||
position: relative;
|
||||
|
||||
// preview/edit buttons
|
||||
> a {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 8px;
|
||||
.comment-toolbar,
|
||||
.nav-links {
|
||||
border-color: $focus-border-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.discussion-form {
|
||||
padding: $gl-padding-top $gl-padding;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.note-edit-form {
|
||||
display: none;
|
||||
font-size: 15px;
|
||||
@@ -152,11 +137,49 @@
|
||||
}
|
||||
}
|
||||
|
||||
.comment-hints {
|
||||
color: #999;
|
||||
background: #fff;
|
||||
padding: 7px;
|
||||
margin-top: -7px;
|
||||
border: 1px solid $border-color;
|
||||
font-size: 13px;
|
||||
.comment-toolbar {
|
||||
padding-top: $gl-padding-top;
|
||||
color: $note-toolbar-color;
|
||||
border-top: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
.timeline-content {
|
||||
margin-left: 55px;
|
||||
|
||||
&.timeline-content-form {
|
||||
@media (max-width: $screen-sm-max) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note-created-ago, .note-updated-at {
|
||||
@@ -76,7 +82,7 @@ ul.notes {
|
||||
// On diffs code should wrap nicely and not overflow
|
||||
pre {
|
||||
code {
|
||||
white-space: pre-wrap;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +155,7 @@ ul.notes {
|
||||
&.notes_content {
|
||||
background-color: #fff;
|
||||
border-width: 1px 0;
|
||||
padding-top: 0;
|
||||
padding: 0;
|
||||
vertical-align: top;
|
||||
&.parallel {
|
||||
border-width: 1px;
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
.git-empty {
|
||||
margin: 0 7px;
|
||||
margin: 0 7px 7px;
|
||||
|
||||
h5 {
|
||||
color: #5c5d5e;
|
||||
@@ -401,7 +401,7 @@ pre.light-well {
|
||||
}
|
||||
|
||||
.commit_short_id {
|
||||
margin-right: 5px;
|
||||
margin: 0 5px;
|
||||
color: $gl-link-color;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -135,25 +135,25 @@
|
||||
|
||||
.location-badge {
|
||||
@include transition(all .15s);
|
||||
background-color: $location-active-bg;
|
||||
background-color: $location-badge-active-bg;
|
||||
color: $white-light;
|
||||
}
|
||||
|
||||
.search-input-wrap {
|
||||
i {
|
||||
color: $location-active-color;
|
||||
color: $location-icon-active-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-location-badge {
|
||||
.search-icon {
|
||||
display: none;
|
||||
}
|
||||
&.has-value {
|
||||
.search-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.clear-icon {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
}
|
||||
.clear-icon {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.container-fluid .content {
|
||||
.container-fluid {
|
||||
.ci-status {
|
||||
padding: 2px 7px;
|
||||
margin-right: 5px;
|
||||
|
||||
@@ -5,7 +5,7 @@ class Admin::ProjectsController < Admin::ApplicationController
|
||||
def index
|
||||
@projects = Project.all
|
||||
@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.abandoned if params[:abandoned].present?
|
||||
@projects = @projects.non_archived unless params[:with_archived].present?
|
||||
|
||||
@@ -55,7 +55,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||
end
|
||||
else
|
||||
saml_user = Gitlab::Saml::User.new(oauth)
|
||||
saml_user.save
|
||||
saml_user.save if saml_user.changed?
|
||||
@user = saml_user.gl_user
|
||||
|
||||
continue_login_process
|
||||
|
||||
@@ -68,7 +68,9 @@ class Projects::ApplicationController < ApplicationController
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def require_branch_head
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
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
|
||||
badge = Gitlab::Badge::Build.new(project, params[:ref])
|
||||
|
||||
@@ -48,7 +48,7 @@ class Projects::BranchesController < Projects::ApplicationController
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to namespace_project_branches_path(@project.namespace,
|
||||
@project)
|
||||
@project), status: 303
|
||||
end
|
||||
format.js { render status: status[:return_code] }
|
||||
end
|
||||
|
||||
@@ -94,9 +94,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
|
||||
end
|
||||
|
||||
def apply_import
|
||||
giver = Project.find(params[:source_project_id])
|
||||
status = @project.team.import(giver, current_user)
|
||||
notice = status ? "Successfully imported" : "Import failed"
|
||||
source_project = Project.find(params[:source_project_id])
|
||||
|
||||
if can?(current_user, :read_project_member, source_project)
|
||||
status = @project.team.import(source_project, current_user)
|
||||
notice = status ? "Successfully imported" : "Import failed"
|
||||
else
|
||||
return render_404
|
||||
end
|
||||
|
||||
redirect_to(namespace_project_project_members_path(project.namespace, project),
|
||||
notice: notice)
|
||||
|
||||
@@ -24,6 +24,8 @@ class Projects::RefsController < Projects::ApplicationController
|
||||
namespace_project_find_file_path(@project.namespace, @project, @id)
|
||||
when "graphs_commits"
|
||||
commits_namespace_project_graph_path(@project.namespace, @project, @id)
|
||||
when "badges"
|
||||
namespace_project_badges_path(@project.namespace, @project, ref: @id)
|
||||
else
|
||||
namespace_project_commits_path(@project.namespace, @project, @id)
|
||||
end
|
||||
|
||||
@@ -88,6 +88,20 @@ class Projects::WikisController < Projects::ApplicationController
|
||||
)
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ class ProjectsController < Projects::ApplicationController
|
||||
def update
|
||||
status = ::Projects::UpdateService.new(@project, current_user, project_params).execute
|
||||
|
||||
# Refresh the repo in case anything changed
|
||||
@repository = project.repository
|
||||
|
||||
respond_to do |format|
|
||||
if status
|
||||
flash[:notice] = "Project '#{@project.name}' was successfully updated."
|
||||
|
||||
@@ -50,12 +50,15 @@ class BuildsEmailService < Service
|
||||
|
||||
def execute(push_data)
|
||||
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(
|
||||
push_data[:build_id],
|
||||
all_recipients(push_data),
|
||||
push_data,
|
||||
recipients,
|
||||
push_data
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -84,7 +87,7 @@ class BuildsEmailService < Service
|
||||
end
|
||||
|
||||
def all_recipients(data)
|
||||
all_recipients = recipients.split(',')
|
||||
all_recipients = recipients.split(',').compact.reject(&:blank?)
|
||||
|
||||
if add_pusher? && data[:user][:email]
|
||||
all_recipients << "#{data[:user][:email]}"
|
||||
|
||||
@@ -55,15 +55,15 @@ class GitPushService < BaseService
|
||||
end
|
||||
|
||||
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 push_to_new_branch? || push_to_existing_branch?
|
||||
|
||||
current_language = @project.repository.main_language
|
||||
|
||||
unless current_language == @project.main_language
|
||||
return @project.update_attributes(main_language: current_language)
|
||||
end
|
||||
|
||||
@project.update_attributes(main_language: current_language)
|
||||
true
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- if controller.controller_path =~ /^groups/
|
||||
- if controller.controller_path =~ /^groups/ && @group.persisted?
|
||||
- label = 'This group'
|
||||
- if controller.controller_path =~ /^projects/
|
||||
- if controller.controller_path =~ /^projects/ && @project.persisted?
|
||||
- label = 'This project'
|
||||
|
||||
.search.search-form{class: "#{'has-location-badge' if label.present?}"}
|
||||
@@ -21,8 +21,8 @@
|
||||
%a.is-focused.dropdown-menu-empty-link
|
||||
Loading...
|
||||
= dropdown_loading
|
||||
%i.search-icon
|
||||
%i.clear-icon.js-clear-input
|
||||
%i.search-icon
|
||||
%i.clear-icon.js-clear-input
|
||||
|
||||
= hidden_field_tag :group_id, @group.try(:id)
|
||||
= hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id'
|
||||
|
||||
@@ -51,8 +51,13 @@
|
||||
= icon('code fw')
|
||||
%span
|
||||
Variables
|
||||
= nav_link path: 'triggers#index' do
|
||||
= nav_link(controller: :triggers) do
|
||||
= link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do
|
||||
= icon('retweet fw')
|
||||
%span
|
||||
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 @@
|
||||
|
||||
- content_for :scripts_body_top do
|
||||
- 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
|
||||
:javascript
|
||||
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
|
||||
= render "layouts/init_auto_complete" if current_user
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
.md-area
|
||||
.md-header.clearfix
|
||||
.md-header
|
||||
%ul.nav-links
|
||||
%li.active
|
||||
%a.js-md-write-button(href="#md-write-holder" tabindex="-1")
|
||||
%a.js-md-write-button{ href: "#md-write-holder" }
|
||||
Write
|
||||
%li
|
||||
%a.js-md-preview-button(href="#md-preview-holder" tabindex="-1")
|
||||
%a.js-md-preview-button{ href: "#md-preview-holder" }
|
||||
Preview
|
||||
%li.pull-right
|
||||
%button.zen-cotrol.zen-control-full.js-zen-enter{ type: 'button' }
|
||||
Go full screen
|
||||
|
||||
%div
|
||||
.md-write-holder
|
||||
= yield
|
||||
.md.md-preview-holder.hide
|
||||
.js-md-preview{class: (preview_class if defined?(preview_class))}
|
||||
.md-write-holder
|
||||
= yield
|
||||
.md.md-preview-holder.js-md-preview.hide{class: (preview_class if defined?(preview_class))}
|
||||
|
||||
- if defined?(referenced_users) && referenced_users
|
||||
%div.referenced-users.hide
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
.zennable
|
||||
.zen-backdrop
|
||||
- classes << ' js-gfm-input js-autosize markdown-area'
|
||||
- if defined?(f) && f
|
||||
= f.text_area attr, class: classes
|
||||
- else
|
||||
= text_area_tag attr, nil, class: classes
|
||||
%a.js-zen-enter(tabindex="-1" href="#")
|
||||
= icon('expand')
|
||||
Edit in fullscreen
|
||||
%a.js-zen-leave(tabindex="-1" href="#")
|
||||
= icon('compress')
|
||||
.zen-backdrop
|
||||
- classes << ' js-gfm-input js-autosize markdown-area'
|
||||
- if defined?(f) && f
|
||||
= f.text_area attr, class: classes, placeholder: "Write a comment or drag your files here..."
|
||||
- else
|
||||
= text_area_tag attr, nil, class: classes, placeholder: "Write a comment or drag your files here..."
|
||||
%a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" }
|
||||
= icon('compress')
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
- 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 ·
|
||||
= @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 @@
|
||||
|
||||
if(typeof merge_request_widget === 'undefined') {
|
||||
merge_request_widget = new MergeRequestWidget(opts);
|
||||
} else {
|
||||
merge_request_widget.setOpts(opts);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
.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)
|
||||
= 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'
|
||||
|
||||
.note-form-actions.clearfix
|
||||
= 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
= 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 :line_type
|
||||
= note_target_fields(@note)
|
||||
@@ -8,7 +8,7 @@
|
||||
= f.hidden_field :noteable_type
|
||||
|
||||
= 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'
|
||||
.error-alert
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
.comment-hints.clearfix
|
||||
.pull-left
|
||||
.comment-toolbar.clearfix
|
||||
.toolbar-text
|
||||
Styling with
|
||||
= link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1
|
||||
tip:
|
||||
= random_markdown_tip
|
||||
.pull-right
|
||||
= link_to '#', class: 'markdown-selector', tabindex: -1 do
|
||||
= icon('paperclip')
|
||||
Attach a file
|
||||
is supported
|
||||
%button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' }
|
||||
= icon('file-image-o', class: 'toolbar-button-icon')
|
||||
Attach a file
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
= access
|
||||
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
|
||||
= 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')
|
||||
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
|
||||
.note-text
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
%ul#notes-list.notes.main-notes-list.timeline
|
||||
= render "projects/notes/notes"
|
||||
.js-notes-busy
|
||||
|
||||
.js-main-target-form
|
||||
- if can? current_user, :create_note, @project
|
||||
= render "projects/notes/form", view: diff_view
|
||||
- else
|
||||
.disabled-comment-area
|
||||
.disabled-profile
|
||||
.disabled-comment
|
||||
%span
|
||||
Please
|
||||
= link_to "register",new_user_session_path
|
||||
or
|
||||
= link_to "login",new_user_session_path
|
||||
to post a comment
|
||||
%ul.notes.timeline
|
||||
%li.timeline-entry
|
||||
- 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
|
||||
- else
|
||||
.disabled-comment.text-center
|
||||
.disabled-comment-text.inline
|
||||
Please
|
||||
= link_to "register",new_user_session_path
|
||||
or
|
||||
= link_to "login",new_user_session_path
|
||||
to post a comment
|
||||
|
||||
:javascript
|
||||
var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
|
||||
|
||||
@@ -345,6 +345,8 @@ production: &base
|
||||
#
|
||||
# - { name: 'saml',
|
||||
# label: 'Our SAML Provider',
|
||||
# groups_attribute: 'Groups',
|
||||
# external_groups: ['Contractors', 'Freelancers'],
|
||||
# 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',
|
||||
@@ -352,6 +354,7 @@ production: &base
|
||||
# issuer: 'https://gitlab.example.com',
|
||||
# name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
|
||||
# } }
|
||||
#
|
||||
# - { name: 'crowd',
|
||||
# args: {
|
||||
# crowd_server_url: 'CROWD SERVER URL',
|
||||
|
||||
@@ -3,5 +3,6 @@ Premailer::Rails.config.merge!(
|
||||
generate_text_part: false,
|
||||
preserve_styles: true,
|
||||
remove_comments: true,
|
||||
remove_ids: true
|
||||
remove_ids: true,
|
||||
remove_scripts: false
|
||||
)
|
||||
|
||||
+5
-3
@@ -575,6 +575,7 @@ Rails.application.routes.draw do
|
||||
# Order matters to give priority to these matches
|
||||
get '/wikis/git_access', to: 'wikis#git_access'
|
||||
get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
|
||||
post '/wikis/markdown_preview', to:'wikis#markdown_preview'
|
||||
post '/wikis', to: 'wikis#create'
|
||||
|
||||
get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
|
||||
@@ -749,10 +750,11 @@ Rails.application.routes.draw do
|
||||
end
|
||||
|
||||
resources :runner_projects, only: [:create, :destroy]
|
||||
resources :badges, only: [], path: 'badges/*ref',
|
||||
constraints: { ref: Gitlab::Regex.git_reference_regex } do
|
||||
resources :badges, only: [:index] do
|
||||
collection do
|
||||
get :build, constraints: { format: /svg/ }
|
||||
scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
|
||||
get :build, constraints: { format: /svg/ }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+16
-4
@@ -7,8 +7,24 @@ Returns a list of project milestones.
|
||||
```
|
||||
GET /projects/:id/milestones
|
||||
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
|
||||
[
|
||||
{
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ GET /users
|
||||
"state": "blocked",
|
||||
"created_at": "2012-05-23T08:01:01Z",
|
||||
"bio": null,
|
||||
"location": null,
|
||||
"skype": "",
|
||||
"linkedin": "",
|
||||
"twitter": "",
|
||||
@@ -126,6 +127,7 @@ Parameters:
|
||||
"created_at": "2012-05-23T08:00:58Z",
|
||||
"is_admin": false,
|
||||
"bio": null,
|
||||
"location": null,
|
||||
"skype": "",
|
||||
"linkedin": "",
|
||||
"twitter": "",
|
||||
@@ -154,6 +156,7 @@ Parameters:
|
||||
"confirmed_at": "2012-05-23T08:00:58Z",
|
||||
"last_sign_in_at": "2015-03-23T08:00:58Z",
|
||||
"bio": null,
|
||||
"location": null,
|
||||
"skype": "",
|
||||
"linkedin": "",
|
||||
"twitter": "",
|
||||
@@ -191,6 +194,7 @@ Parameters:
|
||||
- `extern_uid` (optional) - External UID
|
||||
- `provider` (optional) - External provider name
|
||||
- `bio` (optional) - User's biography
|
||||
- `location` (optional) - User's location
|
||||
- `admin` (optional) - User is admin - true or false (default)
|
||||
- `can_create_group` (optional) - User can create groups - true or false
|
||||
- `confirm` (optional) - Require confirmation - true (default) or false
|
||||
@@ -218,6 +222,7 @@ Parameters:
|
||||
- `extern_uid` - External UID
|
||||
- `provider` - External provider name
|
||||
- `bio` - User's biography
|
||||
- `location` (optional) - User's location
|
||||
- `admin` (optional) - User is admin - true or false (default)
|
||||
- `can_create_group` (optional) - User can create groups - true or false
|
||||
- `external` (optional) - Flags the user as external - true or false(default)
|
||||
@@ -260,6 +265,7 @@ GET /user
|
||||
"state": "active",
|
||||
"created_at": "2012-05-23T08:00:58Z",
|
||||
"bio": null,
|
||||
"location": null,
|
||||
"skype": "",
|
||||
"linkedin": "",
|
||||
"twitter": "",
|
||||
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
- postgres
|
||||
|
||||
before_script:
|
||||
- bundle_install
|
||||
- bundle install
|
||||
|
||||
stages:
|
||||
- build
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
- [CI setup](ci_setup.md) for testing GitLab
|
||||
- [Gotchas](gotchas.md) to avoid
|
||||
- [How to dump production data to staging](db_dump.md)
|
||||
- [Instrumentation](instrumentation.md)
|
||||
- [Migration Style Guide](migration_style_guide.md) for creating safe migrations
|
||||
- [Rake tasks](rake_tasks.md) for development
|
||||
- [Shell commands](shell_commands.md) in the GitLab codebase
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# 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
|
||||
Click the icon to begin the authentication process. If everything goes well the user
|
||||
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
|
||||
|
||||
### `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`
|
||||
|
||||
>**Note:**
|
||||
@@ -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
|
||||
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
|
||||
|
||||
This error means that the IdP doesn't recognize GitLab as a valid sender and
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
_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
|
||||
|
||||
@@ -47,10 +47,10 @@ You can also use other rich text files in GitLab. You might have to install a de
|
||||
|
||||
GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p).
|
||||
|
||||
A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
|
||||
A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
|
||||
Line-breaks, or softreturns, are rendered if you end a line with two or more spaces
|
||||
|
||||
Roses are red [followed by two or more spaces]
|
||||
Roses are red [followed by two or more spaces]
|
||||
Violets are blue
|
||||
|
||||
Sugar is sweet
|
||||
@@ -67,7 +67,7 @@ It is not reasonable to italicize just _part_ of a word, especially when you're
|
||||
perform_complicated_task
|
||||
do_this_and_do_that_and_another_thing
|
||||
|
||||
perform_complicated_task
|
||||
perform_complicated_task
|
||||
do_this_and_do_that_and_another_thing
|
||||
|
||||
## URL auto-linking
|
||||
|
||||
@@ -52,10 +52,11 @@ documentation](../workflow/add-user/add-user.md).
|
||||
| Switch visibility level | | | | | ✓ |
|
||||
| Transfer project to another namespace | | | | | ✓ |
|
||||
| Remove project | | | | | ✓ |
|
||||
| Force push to protected branches | | | | | |
|
||||
| Remove protected branches | | | | | |
|
||||
| Force push to protected branches [^2] | | | | | |
|
||||
| Remove protected branches [^2] | | | | | |
|
||||
|
||||
[^1]: If **Allow guest to access builds** is enabled in CI settings
|
||||
[^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner
|
||||
|
||||
## Group
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ module SharedDiffNote
|
||||
|
||||
step 'I should only see one diff form' 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
|
||||
|
||||
@@ -161,7 +161,7 @@ module SharedDiffNote
|
||||
|
||||
step 'I should see a temporary diff comment form' 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
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ module SharedNote
|
||||
include Spinach::DSL
|
||||
|
||||
step 'I delete a comment' do
|
||||
page.within('.notes') do
|
||||
page.within('.main-notes-list') do
|
||||
find('.note').hover
|
||||
find(".js-note-delete").click
|
||||
end
|
||||
@@ -128,7 +128,7 @@ module SharedNote
|
||||
end
|
||||
|
||||
step 'I edit the last comment with a +1' do
|
||||
page.within(".notes") do
|
||||
page.within(".main-notes-list") do
|
||||
find(".note").hover
|
||||
find('.js-note-edit').click
|
||||
end
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ module API
|
||||
authorize_admin_project
|
||||
|
||||
@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.destroy if protected_branch
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ module API
|
||||
class User < UserBasic
|
||||
expose :created_at
|
||||
expose :is_admin?, as: :is_admin
|
||||
expose :bio, :skype, :linkedin, :twitter, :website_url
|
||||
expose :bio, :location, :skype, :linkedin, :twitter, :website_url
|
||||
end
|
||||
|
||||
class Identity < Grape::Entity
|
||||
|
||||
+18
-2
@@ -3,17 +3,33 @@ module API
|
||||
class Milestones < Grape::API
|
||||
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
|
||||
# Get a list of project milestones
|
||||
#
|
||||
# Parameters:
|
||||
# id (required) - The ID of a project
|
||||
# id (required) - The ID of a project
|
||||
# state (optional) - Return "active" or "closed" milestones
|
||||
# Example Request:
|
||||
# GET /projects/:id/milestones
|
||||
# GET /projects/:id/milestones?state=active
|
||||
# GET /projects/:id/milestones?state=closed
|
||||
get ":id/milestones" do
|
||||
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
|
||||
|
||||
# Get a single project milestone
|
||||
|
||||
+4
-2
@@ -58,6 +58,7 @@ module API
|
||||
# extern_uid - External authentication provider UID
|
||||
# provider - External provider
|
||||
# bio - Bio
|
||||
# location - Location of the user
|
||||
# admin - User is admin - true or false (default)
|
||||
# can_create_group - User can create groups - true or false
|
||||
# confirm - Require user confirmation - true (default) or false
|
||||
@@ -67,7 +68,7 @@ module API
|
||||
post do
|
||||
authenticated_as_admin!
|
||||
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)
|
||||
confirm = !(attrs.delete(:confirm) =~ (/(false|f|no|0)$/i))
|
||||
user = User.build_user(attrs)
|
||||
@@ -106,6 +107,7 @@ module API
|
||||
# website_url - Website url
|
||||
# projects_limit - Limit projects each user can create
|
||||
# bio - Bio
|
||||
# location - Location of the user
|
||||
# admin - User is admin - true or false (default)
|
||||
# can_create_group - User can create groups - true or false
|
||||
# external - Flags the user as external - true or false(default)
|
||||
@@ -114,7 +116,7 @@ module API
|
||||
put ":id" do
|
||||
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])
|
||||
not_found!('User') unless user
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ module Banzai
|
||||
|
||||
elsif element_node?(node)
|
||||
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
|
||||
object_link_filter(link, ref_pattern, link_text: text)
|
||||
end
|
||||
|
||||
@@ -118,7 +118,7 @@ module Banzai
|
||||
end
|
||||
|
||||
if path
|
||||
content_tag(:img, nil, src: path)
|
||||
content_tag(:img, nil, src: path, class: 'gfm')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -144,12 +144,18 @@ module Banzai
|
||||
# if it is not.
|
||||
def process_page_link_tag(parts)
|
||||
if parts.size == 1
|
||||
url = parts[0].strip
|
||||
reference = parts[0].strip
|
||||
else
|
||||
name, url = *parts.compact.map(&:strip)
|
||||
name, reference = *parts.compact.map(&:strip)
|
||||
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
|
||||
|
||||
def project_wiki
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
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
|
||||
module Pipeline
|
||||
class WikiPipeline < FullPipeline
|
||||
def self.filters
|
||||
@filters ||= super.insert_after(Filter::TableOfContentsFilter,
|
||||
Filter::GollumTagsFilter)
|
||||
@filters ||= begin
|
||||
super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter)
|
||||
.insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,12 +4,13 @@ module Gitlab
|
||||
# Build badge
|
||||
#
|
||||
class Build
|
||||
def initialize(project, ref)
|
||||
@image = ::Ci::ImageForBuildService.new.execute(project, ref: ref)
|
||||
end
|
||||
include Gitlab::Application.routes.url_helpers
|
||||
include ActionView::Helpers::AssetTagHelper
|
||||
include ActionView::Helpers::UrlHelper
|
||||
|
||||
def to_s
|
||||
@image[:name].sub(/\.svg$/, '')
|
||||
def initialize(project, ref)
|
||||
@project, @ref = project, ref
|
||||
@image = ::Ci::ImageForBuildService.new.execute(project, ref: ref)
|
||||
end
|
||||
|
||||
def type
|
||||
@@ -19,6 +20,27 @@ module Gitlab
|
||||
def data
|
||||
File.read(@image[:path])
|
||||
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
|
||||
"[](#{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
|
||||
|
||||
@@ -33,7 +33,10 @@ module Gitlab
|
||||
|
||||
def allowed?
|
||||
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
|
||||
if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
|
||||
|
||||
@@ -70,6 +70,32 @@ module Gitlab
|
||||
value.to_s.gsub('=', '\\=')
|
||||
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
|
||||
# "@foo ||= bar" is _not_ thread-safe.
|
||||
if enabled?
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
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
|
||||
@@ -0,0 +1,21 @@
|
||||
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
|
||||
+24
-2
@@ -18,7 +18,7 @@ module Gitlab
|
||||
@user ||= find_or_create_ldap_user
|
||||
end
|
||||
|
||||
if auto_link_saml_enabled?
|
||||
if auto_link_saml_user?
|
||||
@user ||= find_by_email
|
||||
end
|
||||
|
||||
@@ -26,6 +26,16 @@ module Gitlab
|
||||
@user ||= build_new_user
|
||||
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
|
||||
end
|
||||
|
||||
@@ -37,11 +47,23 @@ module Gitlab
|
||||
end
|
||||
end
|
||||
|
||||
def changed?
|
||||
gl_user.changed? || gl_user.identities.any?(&:changed?)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def auto_link_saml_enabled?
|
||||
def auto_link_saml_user?
|
||||
Gitlab.config.omniauth.auto_link_saml_user
|
||||
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
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
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
|
||||
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
|
||||
render_views
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
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
|
||||
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
|
||||
let(:admin) { create(:admin) }
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
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
|
||||
|
||||
before do
|
||||
visit edit_namespace_project_issue_path(project.namespace, project, issue)
|
||||
click_link "Edit"
|
||||
click_button "Go full screen"
|
||||
end
|
||||
|
||||
it 'should open new issue popup' do
|
||||
|
||||
@@ -39,7 +39,7 @@ describe 'GitLab Markdown', feature: true do
|
||||
end
|
||||
|
||||
def doc(html = @html)
|
||||
Nokogiri::HTML::DocumentFragment.parse(html)
|
||||
@doc ||= Nokogiri::HTML::DocumentFragment.parse(html)
|
||||
end
|
||||
|
||||
# Shared behavior that all pipelines should exhibit
|
||||
@@ -230,6 +230,7 @@ describe 'GitLab Markdown', feature: true do
|
||||
file = Gollum::File.new(@project_wiki.wiki)
|
||||
expect(file).to receive(:path).and_return('images/example.jpg')
|
||||
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 })
|
||||
end
|
||||
|
||||
@@ -152,7 +152,7 @@ describe 'Comments', feature: true do
|
||||
|
||||
it 'has .new_note css class' 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
|
||||
@@ -225,6 +225,6 @@ describe 'Comments', feature: true do
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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
|
||||
@@ -1,4 +1,4 @@
|
||||
.zennable
|
||||
.md-area
|
||||
.zen-backdrop
|
||||
%textarea#note_note.js-gfm-input.markdown-area
|
||||
%a.js-zen-enter(tabindex="-1" href="#")
|
||||
|
||||
@@ -29,8 +29,8 @@ describe 'reopen/close issue', ->
|
||||
spyOn(jQuery, 'ajax').and.callFake (req) ->
|
||||
expect(req.type).toBe('PUT')
|
||||
expect(req.url).toBe('http://gitlab.com/issues/6/close')
|
||||
req.success saved: true
|
||||
|
||||
req.success id: 34
|
||||
|
||||
$btnClose = $('a.btn-close')
|
||||
$btnReopen = $('a.btn-reopen')
|
||||
expect($btnReopen).toBeHidden()
|
||||
@@ -94,7 +94,7 @@ describe 'reopen/close issue', ->
|
||||
spyOn(jQuery, 'ajax').and.callFake (req) ->
|
||||
expect(req.type).toBe('PUT')
|
||||
expect(req.url).toBe('http://gitlab.com/issues/6/reopen')
|
||||
req.success saved: true
|
||||
req.success id: 34
|
||||
|
||||
$btnClose = $('a.btn-close')
|
||||
$btnReopen = $('a.btn-reopen')
|
||||
|
||||
@@ -70,20 +70,22 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do
|
||||
end
|
||||
|
||||
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]]'
|
||||
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')['href']).to eq 'wiki-slug'
|
||||
expect(doc.at_css('a')['href']).to eq expected_path
|
||||
end
|
||||
|
||||
it "the created link's text will be link-text" do
|
||||
tag = '[[link-text|wiki-slug]]'
|
||||
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')['href']).to eq 'wiki-slug'
|
||||
expect(doc.at_css('a')['href']).to eq expected_path
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -95,6 +95,14 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
|
||||
result = reference_pipeline_result("Fixed #{reference}")
|
||||
expect(result[:references][:issue]).to eq [issue]
|
||||
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
|
||||
|
||||
context 'cross-project reference' do
|
||||
|
||||
@@ -11,7 +11,7 @@ describe Banzai::Pipeline::WikiPipeline do
|
||||
Foo
|
||||
MD
|
||||
|
||||
result = described_class.call(markdown, project: spy, project_wiki: double)
|
||||
result = described_class.call(markdown, project: spy, project_wiki: spy)
|
||||
|
||||
aggregate_failures do
|
||||
expect(result[:output].text).not_to include '[['
|
||||
@@ -29,7 +29,7 @@ describe Banzai::Pipeline::WikiPipeline do
|
||||
Foo
|
||||
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>]]')
|
||||
end
|
||||
@@ -42,7 +42,7 @@ describe Banzai::Pipeline::WikiPipeline do
|
||||
Foo
|
||||
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
|
||||
expect(output).not_to include('<ul>')
|
||||
|
||||
@@ -3,13 +3,44 @@ require 'spec_helper'
|
||||
describe Gitlab::Badge::Build do
|
||||
let(:project) { create(:project) }
|
||||
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
|
||||
subject { badge.type }
|
||||
it { is_expected.to eq 'image/svg+xml' }
|
||||
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
|
||||
let(:ci_commit) { create(:ci_commit, project: project, sha: sha) }
|
||||
let!(:build) { create(:ci_build, commit: ci_commit) }
|
||||
|
||||
@@ -33,7 +33,7 @@ describe Gitlab::LDAP::Access, lib: true do
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
|
||||
it 'should block user in GitLab' do
|
||||
it 'blocks user in GitLab' do
|
||||
access.allowed?
|
||||
expect(user).to be_blocked
|
||||
expect(user).to be_ldap_blocked
|
||||
@@ -78,6 +78,31 @@ describe Gitlab::LDAP::Access, lib: true do
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
@@ -13,7 +13,7 @@ describe Gitlab::Metrics do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#submit_metrics' do
|
||||
describe '.submit_metrics' do
|
||||
it 'prepares and writes the metrics to InfluxDB' do
|
||||
connection = double(:connection)
|
||||
pool = double(:pool)
|
||||
@@ -26,7 +26,7 @@ describe Gitlab::Metrics do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#prepare_metrics' do
|
||||
describe '.prepare_metrics' do
|
||||
it 'returns a Hash with the keys as Symbols' do
|
||||
metrics = described_class.
|
||||
prepare_metrics([{ 'values' => {}, 'tags' => {} }])
|
||||
@@ -51,7 +51,7 @@ describe Gitlab::Metrics do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#escape_value' do
|
||||
describe '.escape_value' do
|
||||
it 'escapes an equals sign' do
|
||||
expect(described_class.escape_value('foo=')).to eq('foo\\=')
|
||||
end
|
||||
@@ -60,4 +60,45 @@ describe Gitlab::Metrics do
|
||||
expect(described_class.escape_value(10)).to eq('10')
|
||||
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
|
||||
|
||||
@@ -5,7 +5,7 @@ describe Gitlab::Saml::User, lib: true do
|
||||
let(:gl_user) { saml_user.gl_user }
|
||||
let(:uid) { 'my-uid' }
|
||||
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
|
||||
{
|
||||
name: 'John',
|
||||
@@ -23,10 +23,20 @@ describe Gitlab::Saml::User, lib: true do
|
||||
allow(Gitlab::LDAP::Config).to receive_messages(messages)
|
||||
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
|
||||
before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) }
|
||||
let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') }
|
||||
context 'and should bind with SAML' do
|
||||
let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') }
|
||||
it 'adds the SAML identity to the existing user' do
|
||||
saml_user.save
|
||||
expect(gl_user).to be_valid
|
||||
@@ -36,6 +46,35 @@ describe Gitlab::Saml::User, lib: true do
|
||||
expect(identity.provider).to eql 'saml'
|
||||
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
|
||||
|
||||
describe 'no account exists on server' do
|
||||
@@ -68,6 +107,26 @@ describe Gitlab::Saml::User, lib: true do
|
||||
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
|
||||
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'
|
||||
@@ -76,12 +135,6 @@ describe Gitlab::Saml::User, lib: true do
|
||||
context 'with auto_link_ldap_user enabled' do
|
||||
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
|
||||
before { stub_ldap_config(providers: %w(ldapmain)) }
|
||||
|
||||
@@ -89,19 +142,18 @@ describe Gitlab::Saml::User, lib: true do
|
||||
before do
|
||||
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(:email) { %w(john@mail.com john2@example.com) }
|
||||
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)
|
||||
end
|
||||
|
||||
context 'and no account for the LDAP user' do
|
||||
|
||||
it 'creates a user with dual LDAP and SAML identities' do
|
||||
saml_user.save
|
||||
|
||||
expect(gl_user).to be_valid
|
||||
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
|
||||
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' },
|
||||
@@ -111,13 +163,13 @@ describe Gitlab::Saml::User, lib: true do
|
||||
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') }
|
||||
it "adds the omniauth identity to the LDAP account" do
|
||||
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
|
||||
saml_user.save
|
||||
|
||||
expect(gl_user).to be_valid
|
||||
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
|
||||
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' },
|
||||
@@ -126,19 +178,13 @@ describe Gitlab::Saml::User, lib: true do
|
||||
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
|
||||
|
||||
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 'dont block on create' do
|
||||
@@ -162,64 +208,6 @@ describe Gitlab::Saml::User, lib: true do
|
||||
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
|
||||
before do
|
||||
saml_user.save
|
||||
@@ -245,26 +233,6 @@ describe Gitlab::Saml::User, lib: true do
|
||||
expect(gl_user).not_to be_blocked
|
||||
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
|
||||
|
||||
@@ -141,10 +141,12 @@ shared_examples 'a new user email' do
|
||||
end
|
||||
|
||||
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/ }
|
||||
end
|
||||
|
||||
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/ }
|
||||
end
|
||||
|
||||
|
||||
@@ -6,18 +6,38 @@ describe BuildsEmailService do
|
||||
let(:service) { BuildsEmailService.new }
|
||||
|
||||
describe :execute do
|
||||
it "sends email" do
|
||||
it 'sends email' do
|
||||
service.recipients = 'test@gitlab.com'
|
||||
data[:build_status] = 'failed'
|
||||
expect(BuildEmailWorker).to receive(:perform_async)
|
||||
service.execute(data)
|
||||
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_allow_failure] = true
|
||||
expect(BuildEmailWorker).not_to receive(:perform_async)
|
||||
service.execute(data)
|
||||
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
|
||||
|
||||
@@ -4,6 +4,7 @@ describe API::API, api: true do
|
||||
include ApiHelpers
|
||||
let(:user) { create(:user) }
|
||||
let!(:project) { create(:project, namespace: user.namespace ) }
|
||||
let!(:closed_milestone) { create(:closed_milestone, project: project) }
|
||||
let!(:milestone) { create(:milestone, project: project) }
|
||||
|
||||
before { project.team << [user, :developer] }
|
||||
@@ -20,6 +21,24 @@ describe API::API, api: true do
|
||||
get api("/projects/#{project.id}/milestones")
|
||||
expect(response.status).to eq(401)
|
||||
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
|
||||
|
||||
describe 'GET /projects/:id/milestones/:milestone_id' do
|
||||
|
||||
@@ -164,21 +164,37 @@ describe GitPushService, services: true do
|
||||
end
|
||||
|
||||
context "after push" do
|
||||
before do
|
||||
@service = execute_service(project, user, @oldrev, @newrev, ref)
|
||||
def execute
|
||||
execute_service(project, user, @oldrev, @newrev, ref)
|
||||
end
|
||||
|
||||
context "to master" do
|
||||
let(:ref) { @ref }
|
||||
|
||||
it { expect(@service.update_main_language).to eq(true) }
|
||||
it { expect(project.main_language).to eq("Ruby") }
|
||||
context 'when main_language is nil' do
|
||||
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
|
||||
|
||||
context "to other branch" do
|
||||
let(:ref) { 'refs/heads/feature/branch' }
|
||||
|
||||
it { expect(@service.update_main_language).to eq(nil) }
|
||||
it { expect(project.main_language).to eq(nil) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,7 +13,7 @@ module MarkdownMatchers
|
||||
set_default_markdown_messages
|
||||
|
||||
match do |actual|
|
||||
link = actual.at_css('a:contains("Relative Link")')
|
||||
link = actual.at_css('a:contains("Relative Link")')
|
||||
image = actual.at_css('img[alt="Relative Image"]')
|
||||
|
||||
expect(link['href']).to end_with('master/doc/README.md')
|
||||
@@ -72,14 +72,15 @@ module MarkdownMatchers
|
||||
have_css("img[src$='#{src}']")
|
||||
end
|
||||
|
||||
prefix = '/namespace1/gitlabhq/wikis'
|
||||
set_default_markdown_messages
|
||||
|
||||
match do |actual|
|
||||
expect(actual).to have_link('linked-resource', href: 'linked-resource')
|
||||
expect(actual).to have_link('link-text', href: 'linked-resource')
|
||||
expect(actual).to have_link('linked-resource', href: "#{prefix}/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('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')
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user