Commit b2317426 authored by Zeger-Jan van de Weg's avatar Zeger-Jan van de Weg

Merge branch 'master' into add-ability-to-archive-a-project-via-api-14296

parents aabe93ce 54957d69
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.7.0 (unreleased) v 8.7.0 (unreleased)
- Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu)
- Preserve time notes/comments have been updated at when moving issue - Preserve time notes/comments have been updated at when moving issue
- Make HTTP(s) label consistent on clone bar (Stan Hu) - Make HTTP(s) label consistent on clone bar (Stan Hu)
- Allow back dating on issues when created through the API
- Fix avatar stretching by providing a cropping feature - Fix avatar stretching by providing a cropping feature
- Add endpoints to archive or unarchive a project !3372 - Add endpoints to archive or unarchive a project !3372
- Add links to CI setup documentation from project settings and builds pages
- Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
- Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
v 8.6.2 (unreleased) v 8.6.2 (unreleased)
- Comments on confidential issues don't show up in activity feed to non-members - Comments on confidential issues don't show up in activity feed to non-members
- Fix NoMethodError when visiting CI root path at `/ci`
v 8.6.1 v 8.6.1
- Add option to reload the schema before restoring a database backup. !2807 - Add option to reload the schema before restoring a database backup. !2807
......
...@@ -234,7 +234,7 @@ end ...@@ -234,7 +234,7 @@ end
group :development do group :development do
gem "foreman" gem "foreman"
gem 'brakeman', '~> 3.1.0', require: false gem 'brakeman', '~> 3.2.0', require: false
gem "annotate", "~> 2.6.0" gem "annotate", "~> 2.6.0"
gem "letter_opener", '~> 1.1.2' gem "letter_opener", '~> 1.1.2'
...@@ -279,7 +279,7 @@ group :development, :test do ...@@ -279,7 +279,7 @@ group :development, :test do
gem 'capybara-screenshot', '~> 1.0.0' gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.9.0' gem 'poltergeist', '~> 1.9.0'
gem 'teaspoon', '~> 1.0.0' gem 'teaspoon', '~> 1.1.0'
gem 'teaspoon-jasmine', '~> 2.2.0' gem 'teaspoon-jasmine', '~> 2.2.0'
gem 'spring', '~> 1.6.4' gem 'spring', '~> 1.6.4'
......
...@@ -84,21 +84,19 @@ GEM ...@@ -84,21 +84,19 @@ GEM
bootstrap-sass (3.3.6) bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1) autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4) sass (>= 3.3.4)
brakeman (3.1.4) brakeman (3.2.1)
erubis (~> 2.6) erubis (~> 2.6)
fastercsv (~> 1.5)
haml (>= 3.0, < 5.0) haml (>= 3.0, < 5.0)
highline (>= 1.6.20, < 2.0) highline (>= 1.6.20, < 2.0)
multi_json (~> 1.2) ruby2ruby (~> 2.3.0)
ruby2ruby (>= 2.1.1, < 2.3.0) ruby_parser (~> 3.8.1)
ruby_parser (~> 3.7.0)
safe_yaml (>= 1.0) safe_yaml (>= 1.0)
sass (~> 3.0) sass (~> 3.0)
slim (>= 1.3.6, < 4.0) slim (>= 1.3.6, < 4.0)
terminal-table (~> 1.4) terminal-table (~> 1.4)
browser (1.0.1) browser (1.0.1)
builder (3.2.2) builder (3.2.2)
bullet (4.14.10) bullet (5.0.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.9.0) uniform_notifier (~> 1.9.0)
bundler-audit (0.4.0) bundler-audit (0.4.0)
...@@ -208,7 +206,6 @@ GEM ...@@ -208,7 +206,6 @@ GEM
faraday_middleware-multi_json (0.0.6) faraday_middleware-multi_json (0.0.6)
faraday_middleware faraday_middleware
multi_json multi_json
fastercsv (1.5.5)
ffaker (2.0.0) ffaker (2.0.0)
ffi (1.9.10) ffi (1.9.10)
fission (0.5.0) fission (0.5.0)
...@@ -328,8 +325,8 @@ GEM ...@@ -328,8 +325,8 @@ GEM
fog-xml (0.1.2) fog-xml (0.1.2)
fog-core fog-core
nokogiri (~> 1.5, >= 1.5.11) nokogiri (~> 1.5, >= 1.5.11)
font-awesome-rails (4.5.0.0) font-awesome-rails (4.5.0.1)
railties (>= 3.2, < 5.0) railties (>= 3.2, < 5.1)
foreman (0.78.0) foreman (0.78.0)
thor (~> 0.19.1) thor (~> 0.19.1)
formatador (0.2.5) formatador (0.2.5)
...@@ -706,10 +703,10 @@ GEM ...@@ -706,10 +703,10 @@ GEM
ruby-saml (1.1.2) ruby-saml (1.1.2)
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
uuid (~> 2.3) uuid (~> 2.3)
ruby2ruby (2.2.0) ruby2ruby (2.3.0)
ruby_parser (~> 3.1) ruby_parser (~> 3.1)
sexp_processor (~> 4.0) sexp_processor (~> 4.0)
ruby_parser (3.7.2) ruby_parser (3.8.1)
sexp_processor (~> 4.1) sexp_processor (~> 4.1)
rubyntlm (0.5.2) rubyntlm (0.5.2)
rubypants (0.2.0) rubypants (0.2.0)
...@@ -718,7 +715,7 @@ GEM ...@@ -718,7 +715,7 @@ GEM
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (2.1.0) sanitize (2.1.0)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
sass (3.4.20) sass (3.4.21)
sass-rails (5.0.4) sass-rails (5.0.4)
railties (>= 4.0.0, < 5.0) railties (>= 4.0.0, < 5.0)
sass (~> 3.1) sass (~> 3.1)
...@@ -742,7 +739,7 @@ GEM ...@@ -742,7 +739,7 @@ GEM
sentry-raven (0.15.6) sentry-raven (0.15.6)
faraday (>= 0.7.6) faraday (>= 0.7.6)
settingslogic (2.0.9) settingslogic (2.0.9)
sexp_processor (4.6.0) sexp_processor (4.7.0)
sham_rack (1.3.6) sham_rack (1.3.6)
rack rack
shoulda-matchers (2.8.0) shoulda-matchers (2.8.0)
...@@ -806,8 +803,8 @@ GEM ...@@ -806,8 +803,8 @@ GEM
systemu (2.6.5) systemu (2.6.5)
task_list (1.0.2) task_list (1.0.2)
html-pipeline html-pipeline
teaspoon (1.0.2) teaspoon (1.1.5)
railties (>= 3.2.5, < 5) railties (>= 3.2.5, < 6)
teaspoon-jasmine (2.2.0) teaspoon-jasmine (2.2.0)
teaspoon (>= 1.0.0) teaspoon (>= 1.0.0)
temple (0.7.6) temple (0.7.6)
...@@ -868,7 +865,7 @@ GEM ...@@ -868,7 +865,7 @@ GEM
equalizer (~> 0.0, >= 0.0.9) equalizer (~> 0.0, >= 0.0.9)
warden (1.2.4) warden (1.2.4)
rack (>= 1.0) rack (>= 1.0)
web-console (2.2.1) web-console (2.3.0)
activemodel (>= 4.0) activemodel (>= 4.0)
binding_of_caller (>= 0.7.2) binding_of_caller (>= 0.7.2)
railties (>= 4.0) railties (>= 4.0)
...@@ -910,7 +907,7 @@ DEPENDENCIES ...@@ -910,7 +907,7 @@ DEPENDENCIES
better_errors (~> 1.0.1) better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2) binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0) bootstrap-sass (~> 3.3.0)
brakeman (~> 3.1.0) brakeman (~> 3.2.0)
browser (~> 1.0.0) browser (~> 1.0.0)
bullet bullet
bundler-audit bundler-audit
...@@ -1048,7 +1045,7 @@ DEPENDENCIES ...@@ -1048,7 +1045,7 @@ DEPENDENCIES
sprockets (~> 3.3.5) sprockets (~> 3.3.5)
state_machines-activerecord (~> 0.3.0) state_machines-activerecord (~> 0.3.0)
task_list (~> 1.0.2) task_list (~> 1.0.2)
teaspoon (~> 1.0.0) teaspoon (~> 1.1.0)
teaspoon-jasmine (~> 2.2.0) teaspoon-jasmine (~> 2.2.0)
test_after_commit (~> 0.4.2) test_after_commit (~> 0.4.2)
thin (~> 1.6.1) thin (~> 1.6.1)
...@@ -1064,3 +1061,6 @@ DEPENDENCIES ...@@ -1064,3 +1061,6 @@ DEPENDENCIES
web-console (~> 2.0) web-console (~> 2.0)
webmock (~> 1.21.0) webmock (~> 1.21.0)
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH
1.11.2
8.6.0-pre 8.7.0-pre
...@@ -126,8 +126,10 @@ class GitLabDropdown ...@@ -126,8 +126,10 @@ class GitLabDropdown
@selectFirstRow() @selectFirstRow()
# Event listeners # Event listeners
@dropdown.on "shown.bs.dropdown", @opened @dropdown.on "shown.bs.dropdown", @opened
@dropdown.on "hidden.bs.dropdown", @hidden @dropdown.on "hidden.bs.dropdown", @hidden
@dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
@dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) => @dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) =>
...@@ -143,10 +145,11 @@ class GitLabDropdown ...@@ -143,10 +145,11 @@ class GitLabDropdown
selector = ".dropdown-page-one .dropdown-content a" selector = ".dropdown-page-one .dropdown-content a"
@dropdown.on "click", selector, (e) -> @dropdown.on "click", selector, (e) ->
e.preventDefault()
self.rowClicked $(@) self.rowClicked $(@)
if self.options.clicked if self.options.clicked
self.options.clicked() self.options.clicked.call(@,e)
toggleLoading: -> toggleLoading: ->
$('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
...@@ -176,6 +179,15 @@ class GitLabDropdown ...@@ -176,6 +179,15 @@ class GitLabDropdown
@appendMenu(full_html) @appendMenu(full_html)
shouldPropagate: (e) =>
if @options.multiSelect
$target = $(e.target)
if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon')
e.stopPropagation()
return false
else
return true
opened: => opened: =>
contentHtml = $('.dropdown-content', @dropdown).html() contentHtml = $('.dropdown-content', @dropdown).html()
if @remote && contentHtml is "" if @remote && contentHtml is ""
...@@ -184,7 +196,7 @@ class GitLabDropdown ...@@ -184,7 +196,7 @@ class GitLabDropdown
if @options.filterable if @options.filterable
@dropdown.find(".dropdown-input-field").focus() @dropdown.find(".dropdown-input-field").focus()
hidden: => hidden: (e) =>
if @options.filterable if @options.filterable
@dropdown @dropdown
.find(".dropdown-input-field") .find(".dropdown-input-field")
...@@ -195,6 +207,9 @@ class GitLabDropdown ...@@ -195,6 +207,9 @@ class GitLabDropdown
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
$('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
if @options.hidden
@options.hidden.call(@,e)
# Render the full menu # Render the full menu
renderMenu: (html) -> renderMenu: (html) ->
...@@ -226,6 +241,13 @@ class GitLabDropdown ...@@ -226,6 +241,13 @@ class GitLabDropdown
html = @options.renderRow(data) html = @options.renderRow(data)
else else
selected = if @options.isSelected then @options.isSelected(data) else false selected = if @options.isSelected then @options.isSelected(data) else false
if not selected
value = if @options.id then @options.id(data) else data.id
fieldName = @options.fieldName
field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']")
if field.length
selected = true
url = if @options.url then @options.url(data) else "#" url = if @options.url then @options.url(data) else "#"
text = if @options.text then @options.text(data) else "" text = if @options.text then @options.text(data) else ""
cssClass = ""; cssClass = "";
...@@ -258,26 +280,28 @@ class GitLabDropdown ...@@ -258,26 +280,28 @@ class GitLabDropdown
rowClicked: (el) -> rowClicked: (el) ->
fieldName = @options.fieldName fieldName = @options.fieldName
field = @dropdown.parent().find("input[name='#{fieldName}']") selectedIndex = el.parent().index()
if @renderedData
selectedObject = @renderedData[selectedIndex]
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']")
if el.hasClass(ACTIVE_CLASS) if el.hasClass(ACTIVE_CLASS)
el.removeClass(ACTIVE_CLASS)
field.remove() field.remove()
else else
fieldName = @options.fieldName fieldName = @options.fieldName
selectedIndex = el.parent().index() selectedIndex = el.parent().index()
if @renderedData if @renderedData
selectedObject = @renderedData[selectedIndex] selectedObject = @renderedData[selectedIndex]
selectedObject.selected = true
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
if !value? if !value?
field.remove() field.remove()
if @options.multiSelect if not @options.multiSelect
oldValue = field.val()
if oldValue
value = "#{oldValue},#{value}"
else
@dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
@dropdown.parent().find("input[name='#{fieldName}']").remove()
# Toggle active class for the tick mark # Toggle active class for the tick mark
el.toggleClass "is-active" el.toggleClass "is-active"
...@@ -285,15 +309,15 @@ class GitLabDropdown ...@@ -285,15 +309,15 @@ class GitLabDropdown
# Toggle the dropdown label # Toggle the dropdown label
if @options.toggleLabel if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject) $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject)
if value? if value?
if !field.length if !field.length
# Create hidden input for form # Create hidden input for form
input = "<input type='hidden' name='#{fieldName}' />" input = "<input type='hidden' name='#{fieldName}' value='#{value}' />"
if @options.inputId?
input = $(input)
.attr('id', @options.inputId)
@dropdown.before input @dropdown.before input
@dropdown.parent().find("input[name='#{fieldName}']").val value
selectFirstRow: -> selectFirstRow: ->
selector = '.dropdown-content li:first-child a' selector = '.dropdown-content li:first-child a'
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
......
class @IssuableContext class @IssuableContext
constructor: -> constructor: (currentUser) ->
@initParticipants() @initParticipants()
new UsersSelect(currentUser)
new UsersSelect()
$('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}) $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true})
$(".issuable-sidebar .inline-update").on "change", "select", -> $(".issuable-sidebar .inline-update").on "change", "select", ->
...@@ -11,10 +10,20 @@ class @IssuableContext ...@@ -11,10 +10,20 @@ class @IssuableContext
$(this).submit() $(this).submit()
$(document).on "click",".edit-link", (e) -> $(document).on "click",".edit-link", (e) ->
block = $(@).parents('.block') $block = $(@).parents('.block')
block.find('.selectbox').show() $selectbox = $block.find('.selectbox')
block.find('.value').hide() if $selectbox.is(':visible')
block.find('.js-select2').select2("open") $selectbox.hide()
$block.find('.value').show()
else
$selectbox.show()
$block.find('.value').hide()
if $selectbox.is(':visible')
setTimeout (->
$block.find('.dropdown-menu-toggle').trigger 'click'
), 0
$(".right-sidebar").niceScroll() $(".right-sidebar").niceScroll()
......
...@@ -4,14 +4,20 @@ class @LabelsSelect ...@@ -4,14 +4,20 @@ class @LabelsSelect
$dropdown = $(dropdown) $dropdown = $(dropdown)
projectId = $dropdown.data('project-id') projectId = $dropdown.data('project-id')
labelUrl = $dropdown.data('labels') labelUrl = $dropdown.data('labels')
issueUpdateURL = $dropdown.data('issueUpdate')
selectedLabel = $dropdown.data('selected') selectedLabel = $dropdown.data('selected')
if selectedLabel if selectedLabel?
selectedLabel = selectedLabel.toString().split(',') selectedLabel = selectedLabel.split(',')
newLabelField = $('#new_label_name') newLabelField = $('#new_label_name')
newColorField = $('#new_label_color') newColorField = $('#new_label_color')
showNo = $dropdown.data('show-no') showNo = $dropdown.data('show-no')
showAny = $dropdown.data('show-any') showAny = $dropdown.data('show-any')
defaultLabel = $dropdown.data('default-label') defaultLabel = $dropdown.data('default-label')
abilityName = $dropdown.data('ability-name')
$selectbox = $dropdown.closest('.selectbox')
$block = $selectbox.closest('.block')
$value = $block.find('.value')
$loading = $block.find('.block-loading').fadeOut()
if newLabelField.length if newLabelField.length
$newLabelCreateButton = $('.js-new-label-btn') $newLabelCreateButton = $('.js-new-label-btn')
...@@ -21,6 +27,22 @@ class @LabelsSelect ...@@ -21,6 +27,22 @@ class @LabelsSelect
# Suggested colors in the dropdown to chose from pre-chosen colors # Suggested colors in the dropdown to chose from pre-chosen colors
$('.suggest-colors-dropdown a').on 'click', (e) -> $('.suggest-colors-dropdown a').on 'click', (e) ->
issueURLSplit = issueUpdateURL.split('/') if issueUpdateURL?
if issueUpdateURL
labelHTMLTemplate = _.template(
'<% _.each(labels, function(label){ %>
<a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= label.title %>">
<span class="label color-label" style="background-color: <%= label.color %>;">
<%= label.title %>
</span>
</a>
<% }); %>'
);
labelNoneHTMLTemplate = _.template('<div class="light">None</div>')
if newLabelField.length and $dropdown.hasClass 'js-extra-options'
$('.suggest-colors-dropdown a').on "click", (e) ->
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
newColorField newColorField
...@@ -57,6 +79,23 @@ class @LabelsSelect ...@@ -57,6 +79,23 @@ class @LabelsSelect
# This allows us to enable the button when ready # This allows us to enable the button when ready
enableLabelCreateButton = -> enableLabelCreateButton = ->
if newLabelField.val() isnt '' and newColorField.val() isnt '' if newLabelField.val() isnt '' and newColorField.val() isnt ''
$newLabelError.hide()
$('.js-new-label-btn').disable()
# Create new label with API
Api.newLabel projectId, {
name: newLabelField.val()
color: newColorField.val()
}, (label) ->
$('.js-new-label-btn').enable()
if label.message?
$newLabelError
.text label.message
.show()
else
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
$newLabelCreateButton.enable() $newLabelCreateButton.enable()
else else
$newLabelCreateButton.disable() $newLabelCreateButton.disable()
...@@ -90,11 +129,51 @@ class @LabelsSelect ...@@ -90,11 +129,51 @@ class @LabelsSelect
else else
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click' $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
saveLabelData = ->
selected = $dropdown
.closest('.selectbox')
.find("input[name='#{$dropdown.data('field-name')}']")
.map(->
@value
).get()
data = {}
data[abilityName] = {}
data[abilityName].label_ids = selected
if not selected.length
data[abilityName].label_ids = ['']
$loading.fadeIn()
$.ajax(
type: 'PUT'
url: issueUpdateURL
dataType: 'JSON'
data: data
).done (data) ->
$loading.fadeOut()
$selectbox.hide()
data.issueURLSplit = issueURLSplit
if not data.labels.length
template = labelNoneHTMLTemplate()
else
template = labelHTMLTemplate(data)
href = $value
.show()
.html(template)
$value
.find('a')
.each((i) ->
setTimeout(=>
glAnimate($(@), 'pulse')
,200 * i
)
)
$dropdown.glDropdown( $dropdown.glDropdown(
data: (term, callback) -> data: (term, callback) ->
$.ajax( $.ajax(
url: labelUrl url: labelUrl
).done (data) -> ).done (data) ->
if $dropdown.hasClass 'js-extra-options'
if showNo if showNo
data.unshift( data.unshift(
id: 0 id: 0
...@@ -109,22 +188,19 @@ class @LabelsSelect ...@@ -109,22 +188,19 @@ class @LabelsSelect
if data.length > 2 if data.length > 2
data.splice 2, 0, 'divider' data.splice 2, 0, 'divider'
callback data callback data
renderRow: (label) -> renderRow: (label) ->
if $.isArray(selectedLabel) selectedClass = ''
selected = '' if $selectbox.find("input[type='hidden']\
$.each selectedLabel, (i, selectedLbl) -> [name='#{$dropdown.data('field-name')}']\
selectedLbl = selectedLbl.trim() [value='#{label.id}']").length
if selected is '' and label.title is selectedLbl selectedClass = 'is-active'
selected = 'is-active'
else
selected = if label.title is selectedLabel then 'is-active' else ''
color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else "" color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else ""
"<li> "<li>
<a href='#' class='#{selected}'> <a href='#' class='#{selectedClass}'>
#{color} #{color}
#{label.title} #{label.title}
</a> </a>
...@@ -133,6 +209,7 @@ class @LabelsSelect ...@@ -133,6 +209,7 @@ class @LabelsSelect
search: search:
fields: ['title'] fields: ['title']
selectable: true selectable: true
toggleLabel: (selected) -> toggleLabel: (selected) ->
if selected and selected.title isnt 'Any Label' if selected and selected.title isnt 'Any Label'
selected.title selected.title
...@@ -142,8 +219,19 @@ class @LabelsSelect ...@@ -142,8 +219,19 @@ class @LabelsSelect
id: (label) -> id: (label) ->
if label.isAny? if label.isAny?
'' ''
else else if $dropdown.hasClass "js-filter-submit"
label.title label.title
else
label.id
hidden: ->
$selectbox.hide()
$value.show()
if $dropdown.hasClass 'js-multiselect'
saveLabelData()
multiSelect: $dropdown.hasClass 'js-multiselect'
clicked: -> clicked: ->
page = $('body').data 'page' page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index' isIssueIndex = page is 'projects:issues:index'
...@@ -153,4 +241,9 @@ class @LabelsSelect ...@@ -153,4 +241,9 @@ class @LabelsSelect
Issues.filterResults $dropdown.closest('form') Issues.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit' else if $dropdown.hasClass 'js-filter-submit'
$dropdown.closest('form').submit() $dropdown.closest('form').submit()
else
if $dropdown.hasClass 'js-multiselect'
return
else
saveLabelData()
) )
((w) ->
w.glAnimate = ($el, animation, done) ->
$el
.removeClass()
.addClass(animation + ' animated')
.one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', ->
$(this).removeClass()
return
return
return
) window
\ No newline at end of file
class @MilestoneSelect class @MilestoneSelect
constructor: -> constructor: (currentProject) ->
if currentProject?
_this = @
@currentProject = JSON.parse(currentProject)
$('.js-milestone-select').each (i, dropdown) -> $('.js-milestone-select').each (i, dropdown) ->
$dropdown = $(dropdown) $dropdown = $(dropdown)
projectId = $dropdown.data('project-id') projectId = $dropdown.data('project-id')
milestonesUrl = $dropdown.data('milestones') milestonesUrl = $dropdown.data('milestones')
issueUpdateURL = $dropdown.data('issueUpdate')
selectedMilestone = $dropdown.data('selected') selectedMilestone = $dropdown.data('selected')
showNo = $dropdown.data('show-no') showNo = $dropdown.data('show-no')
showAny = $dropdown.data('show-any') showAny = $dropdown.data('show-any')
useId = $dropdown.data('use-id') useId = $dropdown.data('use-id')
defaultLabel = $dropdown.data('default-label') defaultLabel = $dropdown.data('default-label')
issuableId = $dropdown.data('issuable-id')
abilityName = $dropdown.data('ability-name')
$selectbox = $dropdown.closest('.selectbox')
$block = $selectbox.closest('.block')
$value = $block.find('.value')
$loading = $block.find('.block-loading').fadeOut()
if issueUpdateURL
milestoneLinkTemplate = _.template(
'<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= title %></a>'
)
milestoneLinkNoneTemplate = '<div class="light">None</div>'
$dropdown.glDropdown( $dropdown.glDropdown(
data: (term, callback) -> data: (term, callback) ->
$.ajax( $.ajax(
url: milestonesUrl url: milestonesUrl
).done (data) -> ).done (data) ->
if $dropdown.hasClass "js-extra-options"
if showNo if showNo
data.unshift( data.unshift(
id: '0' id: '0'
...@@ -29,7 +47,6 @@ class @MilestoneSelect ...@@ -29,7 +47,6 @@ class @MilestoneSelect
if data.length > 2 if data.length > 2
data.splice 2, 0, 'divider' data.splice 2, 0, 'divider'
callback(data) callback(data)
filterable: true filterable: true
search: search:
...@@ -53,13 +70,38 @@ class @MilestoneSelect ...@@ -53,13 +70,38 @@ class @MilestoneSelect
milestone.id milestone.id
isSelected: (milestone) -> isSelected: (milestone) ->
milestone.title is selectedMilestone milestone.title is selectedMilestone
clicked: -> hidden: ->
page = $('body').data 'page' $selectbox.hide()
isIssueIndex = page is 'projects:issues:index' $value.show()
isMRIndex = page is page is 'projects:merge_requests:index' clicked: (e) ->
if $dropdown.hasClass 'js-filter-bulk-update'
return
if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) if $dropdown.hasClass 'js-filter-submit'
Issues.filterResults $dropdown.closest('form') $dropdown.parents('form').submit()
else if $dropdown.hasClass 'js-filter-submit' else
$dropdown.closest('form').submit() selected = $selectbox
.find('input[type="hidden"]')
.val()
data = {}
data[abilityName] = {}
data[abilityName].milestone_id = selected
$loading
.fadeIn()
$.ajax(
type: 'PUT'
url: issueUpdateURL
data: data
).done (data) ->
$loading.fadeOut()
$selectbox.hide()
$milestoneLink = $value
.show()
.find('a')
if data.milestone?
data.milestone.namespace = _this.currentProject.namespace
data.milestone.path = _this.currentProject.path
$value.html(milestoneLinkTemplate(data.milestone))
else
$value.html(milestoneLinkNoneTemplate)
) )
\ No newline at end of file
class @UsersSelect class @UsersSelect
constructor: -> constructor: (currentUser) ->
@usersPath = "/autocomplete/users.json" @usersPath = "/autocomplete/users.json"
@userPath = "/autocomplete/users/:id.json" @userPath = "/autocomplete/users/:id.json"
if currentUser?
@currentUser = JSON.parse(currentUser)
$('.js-user-search').each (i, dropdown) => $('.js-user-search').each (i, dropdown) =>
$dropdown = $(dropdown) $dropdown = $(dropdown)
...@@ -12,6 +14,67 @@ class @UsersSelect ...@@ -12,6 +14,67 @@ class @UsersSelect
firstUser = $dropdown.data('first-user') firstUser = $dropdown.data('first-user')
selectedId = $dropdown.data('selected') selectedId = $dropdown.data('selected')
defaultLabel = $dropdown.data('default-label') defaultLabel = $dropdown.data('default-label')
issueURL = $dropdown.data('issueUpdate')
$selectbox = $dropdown.closest('.selectbox')
$block = $selectbox.closest('.block')
abilityName = $dropdown.data('ability-name')
$value = $block.find('.value')
$loading = $block.find('.block-loading').fadeOut()
$block.on('click', '.js-assign-yourself', (e) =>
e.preventDefault()
assignTo(@currentUser.id)
)
assignTo = (selected) ->
data = {}
data[abilityName] = {}
data[abilityName].assignee_id = selected
$loading
.fadeIn()
$.ajax(
type: 'PUT'
dataType: 'json'
url: issueURL
data: data
).done (data) ->
$loading.fadeOut()
$selectbox.hide()
if data.assignee
user =
name: data.assignee.name
username: data.assignee.username
avatar: data.assignee.avatar_url
else
user =
name: 'Unassigned'
username: ''
avatar: ''
$value.html(noAssigneeTemplate(user))
$value.find('a').attr('href')
noAssigneeTemplate = _.template(
'<% if (username) { %>
<a class="author_link " href="/u/<%= username %>">
<% if( avatar ) { %>
<img width="32" class="avatar avatar-inline s32" alt="" src="<%= avatar %>">
<% } %>
<span class="author"><%= name %></span>
<span class="username">
@<%= username %>
</span>
</a>
<% } else { %>
<span class="assign-yourself">
No assignee -
<a href="#" class="js-assign-yourself">
assign yourself
</a>
</span>
<% } %>'
)
$dropdown.glDropdown( $dropdown.glDropdown(
data: (term, callback) => data: (term, callback) =>
...@@ -57,20 +120,36 @@ class @UsersSelect ...@@ -57,20 +120,36 @@ class @UsersSelect
fields: ['name', 'username'] fields: ['name', 'username']
selectable: true selectable: true
fieldName: $dropdown.data('field-name') fieldName: $dropdown.data('field-name')
toggleLabel: (selected) -> toggleLabel: (selected) ->
if selected && 'id' of selected if selected && 'id' of selected
selected.name selected.name
else else
defaultLabel defaultLabel
inputId: 'issue_assignee_id'
hidden: (e) ->
$selectbox.hide()
$value.show()
clicked: -> clicked: ->
page = $('body').data 'page' page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index' isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is page is 'projects:merge_requests:index' isMRIndex = page is page is 'projects:merge_requests:index'
if $dropdown.hasClass('js-filter-bulk-update')
return
if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
Issues.filterResults $dropdown.closest('form') Issues.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit' else if $dropdown.hasClass 'js-filter-submit'
$dropdown.closest('form').submit() $dropdown.closest('form').submit()
else
selected = $dropdown
.closest('.selectbox')
.find("input[name='#{$dropdown.data('field-name')}']").val()
assignTo(selected)
renderRow: (user) -> renderRow: (user) ->
username = if user.username then "@#{user.username}" else "" username = if user.username then "@#{user.username}" else ""
avatar = if user.avatar_url then user.avatar_url else false avatar = if user.avatar_url then user.avatar_url else false
...@@ -87,17 +166,25 @@ class @UsersSelect ...@@ -87,17 +166,25 @@ class @UsersSelect
if avatar if avatar
img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />" img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
"<li> # split into three parts so we can remove the username section if nessesary
listWithName = "<li>
<a href='#' class='dropdown-menu-user-link #{selected}'> <a href='#' class='dropdown-menu-user-link #{selected}'>
#{img} #{img}
<strong class='dropdown-menu-user-full-name'> <strong class='dropdown-menu-user-full-name'>
#{user.name} #{user.name}
</strong> </strong>"
<span class='dropdown-menu-user-username'>
listWithUserName = "<span class='dropdown-menu-user-username'>
#{username} #{username}
</span> </span>"
</a> listClosingTags = "</a>
</li>" </li>"
if username is ''
listWithUserName = ''
listWithName + listWithUserName + listClosingTags
) )
$('.ajax-users-select').each (i, select) => $('.ajax-users-select').each (i, select) =>
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
*= require dropzone/basic *= require dropzone/basic
*= require cal-heatmap *= require cal-heatmap
*= require cropper.css *= require cropper.css
*= require animate
*/ */
/* /*
......
...@@ -136,6 +136,10 @@ ...@@ -136,6 +136,10 @@
background-color: $dropdown-empty-row-bg; background-color: $dropdown-empty-row-bg;
} }
} }
&.dropdown-menu-user-link {
line-height: 16px;
}
} }
} }
...@@ -167,13 +171,13 @@ ...@@ -167,13 +171,13 @@
} }
.dropdown-menu-user-link { .dropdown-menu-user-link {
padding-top: 7px; padding-top: 10px;
padding-bottom: 7px; padding-bottom: 7px;
} }
.dropdown-menu-user-full-name { .dropdown-menu-user-full-name {
display: block; display: block;
font-weight: 600; font-weight: 500;
line-height: 16px; line-height: 16px;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
......
...@@ -3,12 +3,10 @@ ...@@ -3,12 +3,10 @@
* *
*/ */
.file-holder { .file-holder {
border: none;
border: 1px solid $border-color; border: 1px solid $border-color;
&.readme-holder { &.readme-holder {
margin-top: 10px; margin: $gl-padding-top 0;
border-bottom: 0;
} }
table { table {
...@@ -50,6 +48,10 @@ ...@@ -50,6 +48,10 @@
} }
} }
a {
color: $gl-dark-link-color;
}
.left-options { .left-options {
margin-top: -3px; margin-top: -3px;
} }
......
...@@ -44,6 +44,7 @@ ...@@ -44,6 +44,7 @@
@include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0); @include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0);
@include border-radius ($border-radius-default); @include border-radius ($border-radius-default);
border: none; border: none;
min-width: 175px;
} }
.select2-results .select2-result-label { .select2-results .select2-result-label {
......
...@@ -93,12 +93,15 @@ li.commit { ...@@ -93,12 +93,15 @@ li.commit {
.commit-row-info { .commit-row-info {
color: $gl-gray; color: $gl-gray;
line-height: 24px; line-height: 24px;
font-size: 13px;
a { a {
color: $gl-gray; color: $gl-gray;
} }
.avatar {
margin-right: 8px;
}
.committed_ago { .committed_ago {
display: inline-block; display: inline-block;
} }
......
...@@ -30,6 +30,10 @@ ...@@ -30,6 +30,10 @@
} }
.issuable-sidebar { .issuable-sidebar {
a {
color: inherit;
}
.block { .block {
@include clearfix; @include clearfix;
padding: $gl-padding 0; padding: $gl-padding 0;
...@@ -89,7 +93,7 @@ ...@@ -89,7 +93,7 @@
} }
.cross-project-reference { .cross-project-reference {
color: $gl-link-color; color: inherit;
span { span {
white-space: nowrap; white-space: nowrap;
...@@ -133,6 +137,12 @@ ...@@ -133,6 +137,12 @@
.value { .value {
line-height: 1; line-height: 1;
.assign-yourself {
margin-top: 10px;
font-weight: normal;
display: block;
}
} }
.bold { .bold {
...@@ -252,6 +262,15 @@ ...@@ -252,6 +262,15 @@
text-decoration: none; text-decoration: none;
} }
} }
.dropdown-menu-toggle {
width: 100%;
padding-top: 6px;
}
.open .dropdown-menu {
width: 100%;
}
} }
.btn-default.gutter-toggle { .btn-default.gutter-toggle {
......
...@@ -230,3 +230,9 @@ ...@@ -230,3 +230,9 @@
} }
} }
} }
.builds {
.table-holder {
overflow-x: scroll;
}
}
...@@ -71,8 +71,6 @@ ...@@ -71,8 +71,6 @@
} }
.note-form-actions { .note-form-actions {
background: #fff;
.note-form-option { .note-form-option {
margin-top: 8px; margin-top: 8px;
margin-left: 30px; margin-left: 30px;
......
...@@ -162,7 +162,7 @@ ...@@ -162,7 +162,7 @@
margin-right: 12px; margin-right: 12px;
a { a {
margin: -1px !important; margin: -1px;
} }
} }
...@@ -222,13 +222,17 @@ ...@@ -222,13 +222,17 @@
padding: 0; padding: 0;
background: transparent; background: transparent;
border: none; border: none;
line-height: 42px; line-height: 36px;
margin: 0; margin: 0;
> li + li:before { > li + li:before {
padding: 0 3px; padding: 0 3px;
color: #999; color: #999;
} }
a {
color: $gl-dark-link-color;
}
} }
.last-push-widget { .last-push-widget {
......
.ci-status { .container-fluid .content {
.ci-status {
padding: 2px 7px; padding: 2px 7px;
margin-right: 5px; margin-right: 5px;
border: 1px solid #eee; border: 1px solid #eee;
...@@ -24,6 +25,8 @@ ...@@ -24,6 +25,8 @@
border-color: $gl-info; border-color: $gl-info;
} }
&.ci-canceled,
&.ci-skipped,
&.ci-disabled { &.ci-disabled {
color: $gl-gray; color: $gl-gray;
border-color: $gl-gray; border-color: $gl-gray;
...@@ -34,21 +37,22 @@ ...@@ -34,21 +37,22 @@
color: $gl-warning; color: $gl-warning;
border-color: $gl-warning; border-color: $gl-warning;
} }
} }
.ci-status-icon-success { .ci-status-icon-success {
@extend .cgreen; color: $gl-success;
} }
.ci-status-icon-failed { .ci-status-icon-failed {
@extend .cred; color: $gl-danger;
} }
.ci-status-icon-running, .ci-status-icon-running,
.ci-status-icon-pending { .ci-status-icon-pending {
// These are standard text color color: $gl-warning;
} }
.ci-status-icon-canceled, .ci-status-icon-canceled,
.ci-status-icon-disabled, .ci-status-icon-disabled,
.ci-status-icon-not-found, .ci-status-icon-not-found,
.ci-status-icon-skipped { .ci-status-icon-skipped {
@extend .cgray; color: $gl-gray;
}
} }
module Ci module Ci
class ProjectsController < Ci::ApplicationController class ProjectsController < Ci::ApplicationController
before_action :project before_action :project
before_action :authorize_read_project!, except: [:badge]
before_action :no_cache, only: [:badge] before_action :no_cache, only: [:badge]
before_action :authorize_read_project!, except: [:badge, :index]
skip_before_action :authenticate_user!, only: [:badge] skip_before_action :authenticate_user!, only: [:badge]
protect_from_forgery protect_from_forgery
def index
redirect_to root_path
end
def show def show
# Temporary compatibility with CI badges pointing to CI project page # Temporary compatibility with CI badges pointing to CI project page
redirect_to namespace_project_path(project.namespace, project) redirect_to namespace_project_path(project.namespace, project)
...@@ -35,5 +39,9 @@ module Ci ...@@ -35,5 +39,9 @@ module Ci
response.headers["Pragma"] = "no-cache" response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
end end
def authorize_read_project!
return access_denied! unless can?(current_user, :read_project, project)
end
end end
end end
...@@ -68,7 +68,13 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -68,7 +68,13 @@ class Projects::IssuesController < Projects::ApplicationController
@merge_requests = @issue.referenced_merge_requests(current_user) @merge_requests = @issue.referenced_merge_requests(current_user)
@related_branches = @issue.related_branches - @merge_requests.map(&:source_branch) @related_branches = @issue.related_branches - @merge_requests.map(&:source_branch)
respond_with(@issue) respond_to do |format|
format.html
format.json do
render json: @issue.to_json(include: [:milestone, :labels])
end
end
end end
def create def create
...@@ -107,10 +113,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -107,10 +113,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
end end
format.json do format.json do
render json: { render json: @issue.to_json(include: [:milestone, :labels, assignee: { methods: :avatar_url }])
saved: @issue.valid?,
assignee_avatar_url: @issue.assignee.try(:avatar_url)
}
end end
end end
end end
......
...@@ -154,10 +154,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -154,10 +154,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.target_project, @merge_request]) @merge_request.target_project, @merge_request])
end end
format.json do format.json do
render json: { render json: @merge_request.to_json(include: [:milestone, :labels, :assignee])
saved: @merge_request.valid?,
assignee_avatar_url: @merge_request.assignee.try(:avatar_url)
}
end end
end end
else else
......
...@@ -19,7 +19,6 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -19,7 +19,6 @@ class Projects::MilestonesController < Projects::ApplicationController
end end
@milestones = @milestones.includes(:project) @milestones = @milestones.includes(:project)
respond_to do |format| respond_to do |format|
format.html do format.html do
@milestones = @milestones.page(params[:page]) @milestones = @milestones.page(params[:page])
......
...@@ -3,7 +3,7 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -3,7 +3,7 @@ class Projects::SnippetsController < Projects::ApplicationController
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read any snippet # Allow read any snippet
before_action :authorize_read_project_snippet! before_action :authorize_read_project_snippet!, except: [:new, :create, :index]
# Allow write(create) snippet # Allow write(create) snippet
before_action :authorize_create_project_snippet!, only: [:new, :create] before_action :authorize_create_project_snippet!, only: [:new, :create]
...@@ -81,6 +81,10 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -81,6 +81,10 @@ class Projects::SnippetsController < Projects::ApplicationController
@snippet ||= @project.snippets.find(params[:id]) @snippet ||= @project.snippets.find(params[:id])
end end
def authorize_read_project_snippet!
return render_404 unless can?(current_user, :read_project_snippet, @snippet)
end
def authorize_update_project_snippet! def authorize_update_project_snippet!
return render_404 unless can?(current_user, :update_project_snippet, @snippet) return render_404 unless can?(current_user, :update_project_snippet, @snippet)
end end
......
...@@ -26,6 +26,10 @@ class RootController < Dashboard::ProjectsController ...@@ -26,6 +26,10 @@ class RootController < Dashboard::ProjectsController
redirect_to activity_dashboard_path redirect_to activity_dashboard_path
when 'starred_project_activity' when 'starred_project_activity'
redirect_to activity_dashboard_path(filter: 'starred') redirect_to activity_dashboard_path(filter: 'starred')
when 'groups'
redirect_to dashboard_groups_path
when 'todos'
redirect_to dashboard_todos_path
else else
return return
end end
......
...@@ -60,7 +60,7 @@ module DropdownsHelper ...@@ -60,7 +60,7 @@ module DropdownsHelper
title_output << content_tag(:span, title) title_output << content_tag(:span, title)
title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do
icon('times') icon('times', class: 'dropdown-menu-close-icon')
end end
title_output.html_safe title_output.html_safe
......
...@@ -16,6 +16,16 @@ module IssuablesHelper ...@@ -16,6 +16,16 @@ module IssuablesHelper
base_issuable_scope(issuable).where('iid > ?', issuable.iid).last base_issuable_scope(issuable).where('iid > ?', issuable.iid).last
end end
def issuable_json_path(issuable)
project = issuable.project
if issuable.kind_of?(MergeRequest)
namespace_project_merge_request_path(project.namespace, project, issuable.iid, :json)
else
namespace_project_issue_path(project.namespace, project, issuable.iid, :json)
end
end
def prev_issuable_for(issuable) def prev_issuable_for(issuable)
base_issuable_scope(issuable).where('iid < ?', issuable.iid).first base_issuable_scope(issuable).where('iid < ?', issuable.iid).first
end end
......
...@@ -12,7 +12,9 @@ module PreferencesHelper ...@@ -12,7 +12,9 @@ module PreferencesHelper
projects: 'Your Projects (default)', projects: 'Your Projects (default)',
stars: 'Starred Projects', stars: 'Starred Projects',
project_activity: "Your Projects' Activity", project_activity: "Your Projects' Activity",
starred_project_activity: "Starred Projects' Activity" starred_project_activity: "Starred Projects' Activity",
groups: "Your Groups",
todos: "Your Todos"
}.with_indifferent_access.freeze }.with_indifferent_access.freeze
# Returns an Array usable by a select field for more user-friendly option text # Returns an Array usable by a select field for more user-friendly option text
......
...@@ -27,6 +27,8 @@ class Ability ...@@ -27,6 +27,8 @@ class Ability
case true case true
when subject.is_a?(PersonalSnippet) when subject.is_a?(PersonalSnippet)
anonymous_personal_snippet_abilities(subject) anonymous_personal_snippet_abilities(subject)
when subject.is_a?(ProjectSnippet)
anonymous_project_snippet_abilities(subject)
when subject.is_a?(CommitStatus) when subject.is_a?(CommitStatus)
anonymous_commit_status_abilities(subject) anonymous_commit_status_abilities(subject)
when subject.is_a?(Project) || subject.respond_to?(:project) when subject.is_a?(Project) || subject.respond_to?(:project)
...@@ -100,6 +102,14 @@ class Ability ...@@ -100,6 +102,14 @@ class Ability
end end
end end
def anonymous_project_snippet_abilities(snippet)
if snippet.public?
[:read_project_snippet]
else
[]
end
end
def global_abilities(user) def global_abilities(user)
rules = [] rules = []
rules << :create_group if user.can_create_group rules << :create_group if user.can_create_group
...@@ -338,25 +348,23 @@ class Ability ...@@ -338,25 +348,23 @@ class Ability
end end
end end
[:note, :project_snippet].each do |name| def note_abilities(user, note)
define_method "#{name}_abilities" do |user, subject|
rules = [] rules = []
if subject.author == user if note.author == user
rules += [ rules += [
:"read_#{name}", :read_note,
:"update_#{name}", :update_note,
:"admin_#{name}" :admin_note
] ]
end end
if subject.respond_to?(:project) && subject.project if note.respond_to?(:project) && note.project
rules += project_abilities(user, subject.project) rules += project_abilities(user, note.project)
end end
rules rules
end end
end
def personal_snippet_abilities(user, snippet) def personal_snippet_abilities(user, snippet)
rules = [] rules = []
...@@ -376,6 +384,24 @@ class Ability ...@@ -376,6 +384,24 @@ class Ability
rules rules
end end
def project_snippet_abilities(user, snippet)
rules = []
if snippet.author == user || user.admin?
rules += [
:read_project_snippet,
:update_project_snippet,
:admin_project_snippet
]
end
if snippet.public? || (snippet.internal? && !user.external?) || (snippet.private? && snippet.project.team.member?(user))
rules << :read_project_snippet
end
rules
end
def group_member_abilities(user, subject) def group_member_abilities(user, subject)
rules = [] rules = []
target_user = subject.user target_user = subject.user
......
...@@ -467,6 +467,18 @@ class Repository ...@@ -467,6 +467,18 @@ class Repository
end end
end end
def gitlab_ci_yml
return nil if !exists? || empty?
@gitlab_ci_yml ||= tree(:head).blobs.find do |file|
file.name == '.gitlab-ci.yml'
end
rescue Rugged::ReferenceError
# For unknow reason spinach scenario "Scenario: I change project path"
# lead to "Reference 'HEAD' not found" exception from Repository#empty?
nil
end
def head_commit def head_commit
@head_commit ||= commit(self.root_ref) @head_commit ||= commit(self.root_ref)
end end
...@@ -877,6 +889,8 @@ class Repository ...@@ -877,6 +889,8 @@ class Repository
end end
def avatar def avatar
return nil unless exists?
@avatar ||= cache.fetch(:avatar) do @avatar ||= cache.fetch(:avatar) do
AVATAR_FILES.find do |file| AVATAR_FILES.find do |file|
blob_at_branch('master', file) blob_at_branch('master', file)
......
...@@ -184,7 +184,7 @@ class User < ActiveRecord::Base ...@@ -184,7 +184,7 @@ class User < ActiveRecord::Base
# User's Dashboard preference # User's Dashboard preference
# Note: When adding an option, it MUST go on the end of the array. # Note: When adding an option, it MUST go on the end of the array.
enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity] enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos]
# User's Project preference # User's Project preference
# Note: When adding an option, it MUST go on the end of the array. # Note: When adding an option, it MUST go on the end of the array.
......
.wiki
%h1
GitLab CI is now integrated in GitLab UI
%h2 For existing projects
%p
Check the following pages to find the CI status you're looking for:
%ul
%li Projects page - shows CI status for each project.
%li Project commits page - show CI status for each commit.
%h2 For new projects
%p
If you want to enable CI for a new project it is easy as adding
= link_to ".gitlab-ci.yml", "http://doc.gitlab.com/ce/ci/yaml/README.html"
file to your repository
%fieldset.builds-feature %fieldset.builds-feature
%legend %legend
Builds: Builds:
- unless @repository.gitlab_ci_yml
.form-group
.col-sm-offset-2.col-sm-10
%p Builds need to be configured before you can begin using Continuous Integration.
= link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
%hr
.form-group .form-group
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
%p Get recent application code using the following command: %p Get recent application code using the following command:
......
...@@ -27,6 +27,9 @@ ...@@ -27,6 +27,9 @@
= link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project),
data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- unless @repository.gitlab_ci_yml
= link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
= link_to ci_lint_path, class: 'btn btn-default' do = link_to ci_lint_path, class: 'btn btn-default' do
= icon('wrench') = icon('wrench')
%span CI Lint %span CI Lint
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do
= icon('code-fork fw') = icon('code-fork fw')
Fork Fork
%div.count-with-arrow = link_to namespace_project_forks_path(@project.namespace, @project), class: 'count-with-arrow' do
%span.arrow %span.arrow
%span.count %span.count
= @project.forks_count = @project.forks_count
...@@ -5,10 +5,10 @@ ...@@ -5,10 +5,10 @@
.panel-heading .panel-heading
Commits (#{@commits.count}) Commits (#{@commits.count})
- if hidden > 0 - if hidden > 0
%ul.well-list %ul.content-list
- commits.each do |commit| - commits.each do |commit|
= render "projects/commits/inline_commit", commit: commit, project: @project = render "projects/commits/inline_commit", commit: commit, project: @project
%li.warning-row.unstyled %li.warning-row.unstyled
#{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
- else - else
%ul.well-list= render commits, project: @project %ul.content-list= render commits, project: @project
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
.light .light
= pluralize(commits.count, 'commit') = pluralize(commits.count, 'commit')
.col-md-10.col-sm-12 .col-md-10.col-sm-12
%ul.bordered-list %ul.content-list
= render commits, project: project = render commits, project: project
%hr.lists-separator %hr.lists-separator
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- header_title project_title(@project, "Files", project_files_path(@project)) - header_title project_title(@project, "Files", project_files_path(@project))
.file-finder-holder.tree-holder.clearfix .file-finder-holder.tree-holder.clearfix
.gray-content-block.top-block .nav-block
.tree-ref-holder .tree-ref-holder
= render 'shared/ref_switcher', destination: 'find_file', path: @path = render 'shared/ref_switcher', destination: 'find_file', path: @path
%ul.breadcrumb.repo-breadcrumb %ul.breadcrumb.repo-breadcrumb
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
.merge-request{'data-url' => merge_request_path(@merge_request)} .merge-request{'data-url' => merge_request_path(@merge_request)}
= render "projects/merge_requests/show/mr_title" = render "projects/merge_requests/show/mr_title"
.merge-request-details.issuable-details .merge-request-details.issuable-details{data: {id: @merge_request.project.id}}
= render "projects/merge_requests/show/mr_box" = render "projects/merge_requests/show/mr_box"
.append-bottom-default.mr-source-target.prepend-top-default .append-bottom-default.mr-source-target.prepend-top-default
- if @merge_request.open? - if @merge_request.open?
......
...@@ -15,11 +15,11 @@ ...@@ -15,11 +15,11 @@
- if current_user - if current_user
%li %li
- if !on_top_of_branch? - if !on_top_of_branch?
%span.btn.btn-sm.add-to-tree.disabled.has-tooltip{title: "You can only add files when you are on a branch", data: { container: 'body' }} %span.btn.add-to-tree.disabled.has-tooltip{title: "You can only add files when you are on a branch", data: { container: 'body' }}
= icon('plus') = icon('plus')
- else - else
%span.dropdown %span.dropdown
%a.dropdown-toggle.btn.btn-sm.add-to-tree{href: '#', "data-toggle" => "dropdown"} %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"}
= icon('plus') = icon('plus')
%ul.dropdown-menu %ul.dropdown-menu
- if can_edit_tree? - if can_edit_tree?
......
...@@ -9,13 +9,13 @@ ...@@ -9,13 +9,13 @@
.filter-item.inline .filter-item.inline
- if params[:author_id] - if params[:author_id]
= hidden_field_tag(:author_id, params[:author_id]) = hidden_field_tag(:author_id, params[:author_id])
= dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author", = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
.filter-item.inline .filter-item.inline
- if params[:assignee_id] - if params[:assignee_id]
= hidden_field_tag(:assignee_id, params[:assignee_id]) = hidden_field_tag(:assignee_id, params[:assignee_id])
= dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee", = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
.filter-item.inline.milestone-filter .filter-item.inline.milestone-filter
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
.filter-item.inline.labels-filter .filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown" = render "shared/issuable/label_dropdown"
.pull-right .pull-right
= render 'shared/sort_dropdown' = render 'shared/sort_dropdown'
...@@ -38,11 +37,10 @@ ...@@ -38,11 +37,10 @@
%li %li
%a{href: "#", data: {id: "close"}} Closed %a{href: "#", data: {id: "close"}} Closed
.filter-item.inline .filter-item.inline
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
.filter-item.inline .filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
= hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag 'update[issues_ids]', []
= hidden_field_tag :state_event, params[:state_event] = hidden_field_tag :state_event, params[:state_event]
.filter-item.inline .filter-item.inline
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
- if issuable.is_a?(MergeRequest) - if issuable.is_a?(MergeRequest)
%p.help-block %p.help-block
.js-wip-explanation .js-wip-explanation
%a.js-toggle-wip{href: ""} %a.js-toggle-wip{href: "", tabindex: -1}
Remove the Remove the
%code WIP: %code WIP:
prefix from the title prefix from the title
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
%strong Work In Progress %strong Work In Progress
merge request to be merged when it's ready. merge request to be merged when it's ready.
.js-no-wip-explanation .js-no-wip-explanation
%a.js-toggle-wip{href: ""} %a.js-toggle-wip{href: "", tabindex: -1}
Start the title with Start the title with
%code WIP: %code WIP:
to prevent a to prevent a
...@@ -127,7 +127,7 @@ ...@@ -127,7 +127,7 @@
for this project. for this project.
- if issuable.new_record? - if issuable.new_record?
= link_to 'Cancel', polymorphic_path([@project.namespace, @project, issuable.class]), class: 'btn btn-cancel' = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
- else - else
.pull-right .pull-right
- if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project) - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project)
......
- if params[:milestone_title] - if params[:milestone_title]
= hidden_field_tag(:milestone_title, params[:milestone_title]) = hidden_field_tag(:milestone_title, params[:milestone_title])
= dropdown_tag(h(params[:milestone_title].presence || "Milestone"), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable", = dropdown_tag(h(params[:milestone_title].presence || "Milestone"), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit js-extra-options', filter: true, dropdown_class: "dropdown-menu-selectable",
placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
- if @project - if @project
%ul.dropdown-footer-list %ul.dropdown-footer-list
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
= icon('user') = icon('user')
.title.hide-collapsed .title.hide-collapsed
Assignee Assignee
= icon('spinner spin', class: 'block-loading')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
= link_to 'Edit', '#', class: 'edit-link pull-right' = link_to 'Edit', '#', class: 'edit-link pull-right'
.value.bold.hide-collapsed .value.bold.hide-collapsed
...@@ -39,10 +40,14 @@ ...@@ -39,10 +40,14 @@
%span.username %span.username
= issuable.assignee.to_reference = issuable.assignee.to_reference
- else - else
.light None %span.assign-yourself
No assignee -
%a.js-assign-yourself{ href: '#' }
assign yourself
.selectbox.hide-collapsed .selectbox.hide-collapsed
= users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true, first_user: true) = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id'
= dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } })
.block.milestone .block.milestone
.sidebar-collapsed-icon .sidebar-collapsed-icon
...@@ -54,6 +59,7 @@ ...@@ -54,6 +59,7 @@
No No
.title.hide-collapsed .title.hide-collapsed
Milestone Milestone
= icon('spinner spin', class: 'block-loading')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
= link_to 'Edit', '#', class: 'edit-link pull-right' = link_to 'Edit', '#', class: 'edit-link pull-right'
.value.bold.hide-collapsed .value.bold.hide-collapsed
...@@ -62,10 +68,10 @@ ...@@ -62,10 +68,10 @@
= issuable.milestone.title = issuable.milestone.title
- else - else
.light None .light None
.selectbox.hide-collapsed .selectbox.hide-collapsed
= f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }}) = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
= hidden_field_tag :issuable_context = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }})
= f.submit class: 'btn hide'
- if issuable.project.labels.any? - if issuable.project.labels.any?
.block.labels .block.labels
...@@ -75,6 +81,7 @@ ...@@ -75,6 +81,7 @@
= issuable.labels.count = issuable.labels.count
.title.hide-collapsed .title.hide-collapsed
Labels Labels
= icon('spinner spin', class: 'block-loading')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
= link_to 'Edit', '#', class: 'edit-link pull-right' = link_to 'Edit', '#', class: 'edit-link pull-right'
.value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) } .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) }
...@@ -84,8 +91,31 @@ ...@@ -84,8 +91,31 @@
- else - else
.light None .light None
.selectbox.hide-collapsed .selectbox.hide-collapsed
= f.collection_select :label_ids, issuable.project.labels.all, :id, :name, - issuable.labels.each do |label|
{ selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" } = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}}
%span.dropdown-toggle-text
Label
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
.dropdown-page-one
= dropdown_title("Assign labels")
= dropdown_filter("Search labels")
= dropdown_content
- if @project
= dropdown_footer do
%ul.dropdown-footer-list
- if can? current_user, :admin_label, @project
%li
%a.dropdown-toggle-page{href: "#"}
Create new
%li
= link_to namespace_project_labels_path(@project.namespace, @project) do
- if can? current_user, :admin_label, @project
Manage labels
- else
View labels
= render "shared/issuable/participants", participants: issuable.participants(current_user) = render "shared/issuable/participants", participants: issuable.participants(current_user)
- if current_user - if current_user
...@@ -116,5 +146,7 @@ ...@@ -116,5 +146,7 @@
= clipboard_button(clipboard_text: project_ref) = clipboard_button(clipboard_text: project_ref)
:javascript :javascript
new Subscription('.subscription'); new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}');
new IssuableContext(); new LabelsSelect();
new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}');
new Subscription('.subscription')
class ConvertClosedToStateInIssue < ActiveRecord::Migration class ConvertClosedToStateInIssue < ActiveRecord::Migration
include Gitlab::Database
def up def up
Issue.transaction do execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value}"
Issue.where(closed: true).update_all(state: :closed) execute "UPDATE #{table_name} SET state = 'opened' WHERE closed = #{false_value}"
Issue.where(closed: false).update_all(state: :opened)
end
end end
def down def down
Issue.transaction do execute "UPDATE #{table_name} SET closed = #{true_value} WHERE state = 'closed'"
Issue.where(state: :closed).update_all(closed: true)
end end
private
def table_name
Issue.table_name
end end
end end
class ConvertClosedToStateInMergeRequest < ActiveRecord::Migration class ConvertClosedToStateInMergeRequest < ActiveRecord::Migration
include Gitlab::Database
def up def up
MergeRequest.transaction do execute "UPDATE #{table_name} SET state = 'merged' WHERE closed = #{true_value} AND merged = #{true_value}"
MergeRequest.where(closed: true, merged: true).update_all(state: :merged) execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value} AND merged = #{false_value}"
MergeRequest.where(closed: true, merged: false).update_all(state: :closed) execute "UPDATE #{table_name} SET state = 'opened' WHERE closed = #{false_value}"
MergeRequest.where(closed: false).update_all(state: :opened)
end
end end
def down def down
MergeRequest.transaction do execute "UPDATE #{table_name} SET closed = #{true_value} WHERE state = 'closed'"
MergeRequest.where(state: :closed).update_all(closed: true) execute "UPDATE #{table_name} SET closed = #{true_value}, merged = #{true_value} WHERE state = 'merged'"
MergeRequest.where(state: :merged).update_all(closed: true, merged: true)
end end
private
def table_name
MergeRequest.table_name
end end
end end
class ConvertClosedToStateInMilestone < ActiveRecord::Migration class ConvertClosedToStateInMilestone < ActiveRecord::Migration
include Gitlab::Database
def up def up
Milestone.transaction do execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value}"
Milestone.where(closed: true).update_all(state: :closed) execute "UPDATE #{table_name} SET state = 'active' WHERE closed = #{false_value}"
Milestone.where(closed: false).update_all(state: :active)
end
end end
def down def down
Milestone.transaction do execute "UPDATE #{table_name} SET closed = #{true_value} WHERE state = 'cloesd'"
Milestone.where(state: :closed).update_all(closed: true)
end end
private
def table_name
Milestone.table_name
end end
end end
class ConvertMergeStatusInMergeRequest < ActiveRecord::Migration class ConvertMergeStatusInMergeRequest < ActiveRecord::Migration
def up def up
MergeRequest.transaction do execute "UPDATE #{table_name} SET new_merge_status = 'unchecked' WHERE merge_status = 1"
MergeRequest.where(merge_status: 1).update_all("new_merge_status = 'unchecked'") execute "UPDATE #{table_name} SET new_merge_status = 'can_be_merged' WHERE merge_status = 2"
MergeRequest.where(merge_status: 2).update_all("new_merge_status = 'can_be_merged'") execute "UPDATE #{table_name} SET new_merge_status = 'cannot_be_merged' WHERE merge_status = 3"
MergeRequest.where(merge_status: 3).update_all("new_merge_status = 'cannot_be_merged'")
end
end end
def down def down
MergeRequest.transaction do execute "UPDATE #{table_name} SET merge_status = 1 WHERE new_merge_status = 'unchecked'"
MergeRequest.where(new_merge_status: :unchecked).update_all("merge_status = 1") execute "UPDATE #{table_name} SET merge_status = 2 WHERE new_merge_status = 'can_be_merged'"
MergeRequest.where(new_merge_status: :can_be_merged).update_all("merge_status = 2") execute "UPDATE #{table_name} SET merge_status = 3 WHERE new_merge_status = 'cannot_be_merged'"
MergeRequest.where(new_merge_status: :cannot_be_merged).update_all("merge_status = 3")
end end
private
def table_name
MergeRequest.table_name
end end
end end
class AllowMergesForForks < ActiveRecord::Migration class AllowMergesForForks < ActiveRecord::Migration
def self.up def self.up
add_column :merge_requests, :target_project_id, :integer, :null => true add_column :merge_requests, :target_project_id, :integer, :null => true
MergeRequest.update_all("target_project_id = project_id") execute "UPDATE #{table_name} SET target_project_id = project_id"
change_column :merge_requests, :target_project_id, :integer, :null => false change_column :merge_requests, :target_project_id, :integer, :null => false
rename_column :merge_requests, :project_id, :source_project_id rename_column :merge_requests, :project_id, :source_project_id
end end
...@@ -10,4 +10,10 @@ class AllowMergesForForks < ActiveRecord::Migration ...@@ -10,4 +10,10 @@ class AllowMergesForForks < ActiveRecord::Migration
remove_column :merge_requests, :target_project_id remove_column :merge_requests, :target_project_id
rename_column :merge_requests, :source_project_id,:project_id rename_column :merge_requests, :source_project_id,:project_id
end end
private
def table_name
MergeRequest.table_name
end
end end
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
## User documentation ## User documentation
- [API](api/README.md) Automate GitLab via a simple and powerful API. - [API](api/README.md) Automate GitLab via a simple and powerful API.
- [CI](ci/README.md) - [CI](ci/README.md) GitLab Continuous Integration (CI) getting started, .gitlab-ci.yml options, and examples.
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
- [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab. - [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
- [Importing to GitLab](workflow/importing/README.md). - [Importing to GitLab](workflow/importing/README.md).
...@@ -19,10 +19,12 @@ ...@@ -19,10 +19,12 @@
## Administrator documentation ## Administrator documentation
- [Authentication/Authorization](administration/auth/README.md) Configure
external authentication with LDAP, SAML, CAS and additional Omniauth providers.
- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when webhooks aren't enough. - [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when webhooks aren't enough.
- [Install](install/README.md) Requirements, directory structures and installation from source. - [Install](install/README.md) Requirements, directory structures and installation from source.
- [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components - [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components
- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, LDAP and Twitter. - [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter.
- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. - [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages.
- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. - [Libravatar](customization/libravatar.md) Use Libravatar for user avatars.
- [Log system](logs/logs.md) Log system. - [Log system](logs/logs.md) Log system.
...@@ -45,4 +47,3 @@ ...@@ -45,4 +47,3 @@
contributing to documentation. contributing to documentation.
- [Development](development/README.md) Explains the architecture and the guidelines for shell commands. - [Development](development/README.md) Explains the architecture and the guidelines for shell commands.
- [Legal](legal/README.md) Contributor license agreements. - [Legal](legal/README.md) Contributor license agreements.
- [Release](release/README.md) How to make the monthly and security releases.
# Authentication and Authorization
GitLab integrates with the following external authentication and authorization
providers.
- [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP,
and 389 Server
- [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google,
Bitbucket, Facebook, Shibboleth, Crowd and Azure
- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS
# LDAP
GitLab integrates with LDAP to support user authentication.
This integration works with most LDAP-compliant directory
servers, including Microsoft Active Directory, Apple Open Directory, Open LDAP,
and 389 Server. GitLab EE includes enhanced integration, including group
membership syncing.
## Security
GitLab assumes that LDAP users are not able to change their LDAP 'mail', 'email'
or 'userPrincipalName' attribute. An LDAP user who is allowed to change their
email on the LDAP server can potentially
[take over any account](#enabling-ldap-sign-in-for-existing-gitlab-users)
on your GitLab server.
We recommend against using LDAP integration if your LDAP users are
allowed to change their 'mail', 'email' or 'userPrincipalName' attribute on
the LDAP server.
### User deletion
If a user is deleted from the LDAP server, they will be blocked in GitLab, as
well. Users will be immediately blocked from logging in. However, there is an
LDAP check cache time (sync time) of one hour (see note). This means users that
are already logged in or are using Git over SSH will still be able to access
GitLab for up to one hour. Manually block the user in the GitLab Admin area to
immediately block all access.
>**Note**: GitLab EE supports a configurable sync time, with a default
of one hour.
## Configuration
To enable LDAP integration you need to add your LDAP server settings in
`/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`.
>**Note**: In GitLab EE, you can configure multiple LDAP servers to connect to
one GitLab server.
Prior to version 7.4, GitLab used a different syntax for configuring
LDAP integration. The old LDAP integration syntax still works but may be
removed in a future version. If your `gitlab.rb` or `gitlab.yml` file contains
LDAP settings in both the old syntax and the new syntax, only the __old__
syntax will be used by GitLab.
The configuration inside `gitlab_rails['ldap_servers']` below is sensitive to
incorrect indentation. Be sure to retain the indentation given in the example.
Copy/paste can sometimes cause problems.
**Omnibus configuration**
```ruby
gitlab_rails['ldap_enabled'] = true
gitlab_rails['ldap_servers'] = YAML.load <<-EOS # remember to close this block with 'EOS' below
main: # 'main' is the GitLab 'provider ID' of this LDAP server
## label
#
# A human-friendly name for your LDAP server. It is OK to change the label later,
# for instance if you find out it is too large to fit on the web page.
#
# Example: 'Paris' or 'Acme, Ltd.'
label: 'LDAP'
host: '_your_ldap_server'
port: 389
uid: 'sAMAccountName'
method: 'plain' # "tls" or "ssl" or "plain"
bind_dn: '_the_full_dn_of_the_user_you_will_bind_with'
password: '_the_password_of_the_bind_user'
# Set a timeout, in seconds, for LDAP queries. This helps avoid blocking
# a request if the LDAP server becomes unresponsive.
# A value of 0 means there is no timeout.
timeout: 10
# This setting specifies if LDAP server is Active Directory LDAP server.
# For non AD servers it skips the AD specific queries.
# If your LDAP server is not AD, set this to false.
active_directory: true
# If allow_username_or_email_login is enabled, GitLab will ignore everything
# after the first '@' in the LDAP username submitted by the user on login.
#
# Example:
# - the user enters 'jane.doe@example.com' and 'p@ssw0rd' as LDAP credentials;
# - GitLab queries the LDAP server with 'jane.doe' and 'p@ssw0rd'.
#
# If you are using "uid: 'userPrincipalName'" on ActiveDirectory you need to
# disable this setting, because the userPrincipalName contains an '@'.
allow_username_or_email_login: false
# To maintain tight control over the number of active users on your GitLab installation,
# enable this setting to keep new users blocked until they have been cleared by the admin
# (default: false).
block_auto_created_users: false
# Base where we can search for users
#
# Ex. ou=People,dc=gitlab,dc=example
#
base: ''
# Filter LDAP users
#
# Format: RFC 4515 https://tools.ietf.org/search/rfc4515
# Ex. (employeeType=developer)
#
# Note: GitLab does not support omniauth-ldap's custom filter syntax.
#
user_filter: ''
# LDAP attributes that GitLab will use to create an account for the LDAP user.
# The specified attribute can either be the attribute name as a string (e.g. 'mail'),
# or an array of attribute names to try in order (e.g. ['mail', 'email']).
# Note that the user's LDAP login will always be the attribute specified as `uid` above.
attributes:
# The username will be used in paths for the user's own projects
# (like `gitlab.example.com/username/project`) and when mentioning
# them in issues, merge request and comments (like `@username`).
# If the attribute specified for `username` contains an email address,
# the GitLab username will be the part of the email address before the '@'.
username: ['uid', 'userid', 'sAMAccountName']
email: ['mail', 'email', 'userPrincipalName']
# If no full name could be found at the attribute specified for `name`,
# the full name is determined using the attributes specified for
# `first_name` and `last_name`.
name: 'cn'
first_name: 'givenName'
last_name: 'sn'
## EE only
# Base where we can search for groups
#
# Ex. ou=groups,dc=gitlab,dc=example
#
group_base: ''
# The CN of a group containing GitLab administrators
#
# Ex. administrators
#
# Note: Not `cn=administrators` or the full DN
#
admin_group: ''
# The LDAP attribute containing a user's public SSH key
#
# Ex. ssh_public_key
#
sync_ssh_keys: false
# GitLab EE only: add more LDAP servers
# Choose an ID made of a-z and 0-9 . This ID will be stored in the database
# so that GitLab can remember which LDAP server a user belongs to.
# uswest2:
# label:
# host:
# ....
EOS
```
**Source configuration**
Use the same format as `gitlab_rails['ldap_servers']` for the contents under
`servers:` in the example below:
```
production:
# snip...
ldap:
enabled: false
servers:
main: # 'main' is the GitLab 'provider ID' of this LDAP server
## label
#
# A human-friendly name for your LDAP server. It is OK to change the label later,
# for instance if you find out it is too large to fit on the web page.
#
# Example: 'Paris' or 'Acme, Ltd.'
label: 'LDAP'
# snip...
```
## Using an LDAP filter to limit access to your GitLab server
If you want to limit all GitLab access to a subset of the LDAP users on your
LDAP server, the first step should be to narrow the configured `base`. However,
it is sometimes necessary to filter users further. In this case, you can set up
an LDAP user filter. The filter must comply with
[RFC 4515](https://tools.ietf.org/search/rfc4515).
**Omnibus configuration**
```ruby
gitlab_rails['ldap_servers'] = YAML.load <<-EOS
main:
# snip...
user_filter: '(employeeType=developer)'
EOS
```
**Source configuration**
```yaml
production:
ldap:
servers:
main:
# snip...
user_filter: '(employeeType=developer)'
```
Tip: If you want to limit access to the nested members of an Active Directory
group you can use the following syntax:
```
(memberOf:1.2.840.113556.1.4.1941:=CN=My Group,DC=Example,DC=com)
```
Please note that GitLab does not support the custom filter syntax used by
omniauth-ldap.
## Enabling LDAP sign-in for existing GitLab users
When a user signs in to GitLab with LDAP for the first time, and their LDAP
email address is the primary email address of an existing GitLab user, then
the LDAP DN will be associated with the existing user. If the LDAP email
attribute is not found in GitLab's database, a new user is created.
In other words, if an existing GitLab user wants to enable LDAP sign-in for
themselves, they should check that their GitLab email address matches their
LDAP email address, and then sign into GitLab via their LDAP credentials.
## Limitations
### TLS Client Authentication
Not implemented by `Net::LDAP`.
You should disable anonymous LDAP authentication and enable simple or SASL
authentication. The TLS client authentication setting in your LDAP server cannot
be mandatory and clients cannot be authenticated with the TLS protocol.
### TLS Server Authentication
Not supported by GitLab's configuration options.
When setting `method: ssl`, the underlying authentication method used by
`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with
the LDAP server before any LDAP-protocol data is exchanged but no validation of
the LDAP server's SSL certificate is performed.
## Troubleshooting
### Invalid credentials when logging in
- Make sure the user you are binding with has enough permissions to read the user's
tree and traverse it.
- Check that the `user_filter` is not blocking otherwise valid users.
- Run the following check command to make sure that the LDAP settings are
correct and GitLab can see your users:
```bash
# For Omnibus installations
sudo gitlab-rake gitlab:ldap:check
# For installations from source
sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production
```
### Connection Refused
If you are getting 'Connection Refused' errors when trying to connect to the
LDAP server please double-check the LDAP `port` and `method` settings used by
GitLab. Common combinations are `method: 'plain'` and `port: 389`, OR
`method: 'ssl'` and `port: 636`.
...@@ -237,6 +237,7 @@ POST /projects/:id/issues ...@@ -237,6 +237,7 @@ POST /projects/:id/issues
| `assignee_id` | integer | no | The ID of a user to assign issue | | `assignee_id` | integer | no | The ID of a user to assign issue |
| `milestone_id` | integer | no | The ID of a milestone to assign issue | | `milestone_id` | integer | no | The ID of a milestone to assign issue |
| `labels` | string | no | Comma-separated label names for an issue | | `labels` | string | no | Comma-separated label names for an issue |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` |
```bash ```bash
curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug
......
...@@ -17,7 +17,7 @@ GitHub will generate an application ID and secret key for you to use. ...@@ -17,7 +17,7 @@ GitHub will generate an application ID and secret key for you to use.
- Application name: This can be anything. Consider something like "\<Organization\>'s GitLab" or "\<Your Name\>'s GitLab" or something else descriptive. - Application name: This can be anything. Consider something like "\<Organization\>'s GitLab" or "\<Your Name\>'s GitLab" or something else descriptive.
- Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com' - Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com'
- Application description: Fill this in if you wish. - Application description: Fill this in if you wish.
- Authorization callback URL: 'https://gitlab.company.com/' - Default authorization callback URL is '${YOUR_DOMAIN}/import/github/callback'
1. Select "Register application". 1. Select "Register application".
1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). 1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
......
# GitLab LDAP integration # GitLab LDAP integration
GitLab can be configured to allow your users to sign with their LDAP credentials to integrate with e.g. Active Directory. This document was moved under [`administration/auth/ldap`](administration/auth/ldap.md).
The first time a user signs in with LDAP credentials, GitLab will create a new GitLab user associated with the LDAP Distinguished Name (DN) of the LDAP user.
GitLab user attributes such as nickname and email will be copied from the LDAP user entry.
## Security
GitLab assumes that LDAP users are not able to change their LDAP 'mail', 'email' or 'userPrincipalName' attribute.
An LDAP user who is allowed to change their email on the LDAP server can [take over any account](#enabling-ldap-sign-in-for-existing-gitlab-users) on your GitLab server.
We recommend against using GitLab LDAP integration if your LDAP users are allowed to change their 'mail', 'email' or 'userPrincipalName' attribute on the LDAP server.
If a user is deleted from the LDAP server, they will be blocked in GitLab as well.
Users will be immediately blocked from logging in. However, there is an LDAP check
cache time of one hour. The means users that are already logged in or are using Git
over SSH will still be able to access GitLab for up to one hour. Manually block
the user in the GitLab Admin area to immediately block all access.
## Configuring GitLab for LDAP integration
To enable GitLab LDAP integration you need to add your LDAP server settings in `/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`.
In GitLab Enterprise Edition you can have multiple LDAP servers connected to one GitLab server.
Please note that before version 7.4, GitLab used a different syntax for configuring LDAP integration.
The old LDAP integration syntax still works in GitLab 7.4.
If your `gitlab.rb` or `gitlab.yml` file contains LDAP settings in both the old syntax and the new syntax, only the __old__ syntax will be used by GitLab.
```ruby
# For omnibus packages
gitlab_rails['ldap_enabled'] = true
gitlab_rails['ldap_servers'] = YAML.load <<-EOS # remember to close this block with 'EOS' below
main: # 'main' is the GitLab 'provider ID' of this LDAP server
## label
#
# A human-friendly name for your LDAP server. It is OK to change the label later,
# for instance if you find out it is too large to fit on the web page.
#
# Example: 'Paris' or 'Acme, Ltd.'
label: 'LDAP'
host: '_your_ldap_server'
port: 389
uid: 'sAMAccountName'
method: 'plain' # "tls" or "ssl" or "plain"
bind_dn: '_the_full_dn_of_the_user_you_will_bind_with'
password: '_the_password_of_the_bind_user'
# Set a timeout, in seconds, for LDAP queries. This helps avoid blocking
# a request if the LDAP server becomes unresponsive.
# A value of 0 means there is no timeout.
timeout: 10
# This setting specifies if LDAP server is Active Directory LDAP server.
# For non AD servers it skips the AD specific queries.
# If your LDAP server is not AD, set this to false.
active_directory: true
# If allow_username_or_email_login is enabled, GitLab will ignore everything
# after the first '@' in the LDAP username submitted by the user on login.
#
# Example:
# - the user enters 'jane.doe@example.com' and 'p@ssw0rd' as LDAP credentials;
# - GitLab queries the LDAP server with 'jane.doe' and 'p@ssw0rd'.
#
# If you are using "uid: 'userPrincipalName'" on ActiveDirectory you need to
# disable this setting, because the userPrincipalName contains an '@'.
allow_username_or_email_login: false
# To maintain tight control over the number of active users on your GitLab installation,
# enable this setting to keep new users blocked until they have been cleared by the admin
# (default: false).
block_auto_created_users: false
# Base where we can search for users
#
# Ex. ou=People,dc=gitlab,dc=example
#
base: ''
# Filter LDAP users
#
# Format: RFC 4515 https://tools.ietf.org/search/rfc4515
# Ex. (employeeType=developer)
#
# Note: GitLab does not support omniauth-ldap's custom filter syntax.
#
user_filter: ''
# LDAP attributes that GitLab will use to create an account for the LDAP user.
# The specified attribute can either be the attribute name as a string (e.g. 'mail'),
# or an array of attribute names to try in order (e.g. ['mail', 'email']).
# Note that the user's LDAP login will always be the attribute specified as `uid` above.
attributes:
# The username will be used in paths for the user's own projects
# (like `gitlab.example.com/username/project`) and when mentioning
# them in issues, merge request and comments (like `@username`).
# If the attribute specified for `username` contains an email address,
# the GitLab username will be the part of the email address before the '@'.
username: ['uid', 'userid', 'sAMAccountName']
email: ['mail', 'email', 'userPrincipalName']
# If no full name could be found at the attribute specified for `name`,
# the full name is determined using the attributes specified for
# `first_name` and `last_name`.
name: 'cn'
first_name: 'givenName'
last_name: 'sn'
# GitLab EE only: add more LDAP servers
# Choose an ID made of a-z and 0-9 . This ID will be stored in the database
# so that GitLab can remember which LDAP server a user belongs to.
# uswest2:
# label:
# host:
# ....
EOS
```
If you are getting 'Connection Refused' errors when trying to connect to the LDAP server please double-check the LDAP `port` and `method` settings used by GitLab.
Common combinations are `method: 'plain'` and `port: 389`, OR `method: 'ssl'` and `port: 636`.
If you are using a GitLab installation from source you can find the LDAP settings in `/home/git/gitlab/config/gitlab.yml`:
```
production:
# snip...
ldap:
enabled: false
servers:
main: # 'main' is the GitLab 'provider ID' of this LDAP server
## label
#
# A human-friendly name for your LDAP server. It is OK to change the label later,
# for instance if you find out it is too large to fit on the web page.
#
# Example: 'Paris' or 'Acme, Ltd.'
label: 'LDAP'
# snip...
```
## Enabling LDAP sign-in for existing GitLab users
When a user signs in to GitLab with LDAP for the first time, and their LDAP email address is the primary email address of an existing GitLab user, then the LDAP DN will be associated with the existing user.
If the LDAP email attribute is not found in GitLab's database, a new user is created.
In other words, if an existing GitLab user wants to enable LDAP sign-in for themselves, they should check that their GitLab email address matches their LDAP email address, and then sign into GitLab via their LDAP credentials.
GitLab recognizes the following LDAP attributes as email addresses: `mail`, `email` and `userPrincipalName`.
If multiple LDAP email attributes are present, e.g. `mail: foo@bar.com` and `email: foo@example.com`, then the first attribute found wins -- in this case `foo@bar.com`.
## Using an LDAP filter to limit access to your GitLab server
If you want to limit all GitLab access to a subset of the LDAP users on your LDAP server you can set up an LDAP user filter.
The filter must comply with [RFC 4515](https://tools.ietf.org/search/rfc4515).
```ruby
# For omnibus packages; new LDAP server syntax
gitlab_rails['ldap_servers'] = YAML.load <<-EOS
main:
# snip...
user_filter: '(employeeType=developer)'
EOS
```
```yaml
# For installations from source; new LDAP server syntax
production:
ldap:
servers:
main:
# snip...
user_filter: '(employeeType=developer)'
```
Tip: if you want to limit access to the nested members of an Active Directory group you can use the following syntax:
```
(memberOf:1.2.840.113556.1.4.1941:=CN=My Group,DC=Example,DC=com)
```
Please note that GitLab does not support the custom filter syntax used by omniauth-ldap.
## Limitations
GitLab's LDAP client is based on [omniauth-ldap](https://gitlab.com/gitlab-org/omniauth-ldap)
which encapsulates Ruby's `Net::LDAP` class. It provides a pure-Ruby implementation
of the LDAP client protocol. As a result, GitLab is limited by `omniauth-ldap` and may impact your LDAP
server settings.
### TLS Client Authentication
Not implemented by `Net::LDAP`.
So you should disable anonymous LDAP authentication and enable simple or SASL
authentication. TLS client authentication setting in your LDAP server cannot be
mandatory and clients cannot be authenticated with the TLS protocol.
### TLS Server Authentication
Not supported by GitLab's configuration options.
When setting `method: ssl`, the underlying authentication method used by
`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with
the LDAP server before any LDAP-protocol data is exchanged but no validation of
the LDAP server's SSL certificate is performed.
## Troubleshooting
### Invalid credentials when logging in
Make sure the user you are binding with has enough permissions to read the user's
tree and traverse it.
Also make sure that the `user_filter` is not blocking otherwise valid users.
To make sure that the LDAP settings are correct and GitLab can see your users,
execute the following command:
```bash
# For Omnibus installations
sudo gitlab-rake gitlab:ldap:check
# For installations from source
sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production
```
## Release cycle
Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases are published when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG). Features that will likely be in the next releases can be found on the [direction page](https://about.gitlab.com/direction/).
## Release process documentation
- [Monthly release](monthly.md), every month on the 22nd.
- [Patch release](patch.md), if there are serious regressions.
- [Security](security.md), for security problems.
- [Master](master.md), update process for the master branch.
# How to create RC1
The RC1 release comes with the task to update the installation and upgrade docs. Be mindful that there might already be merge requests for this on GitLab or GitHub.
### 1. Update the installation guide
1. Check if it references the correct branch `x-x-stable` (doesn't exist yet, but that is okay)
1. Check the [GitLab Shell version](/lib/tasks/gitlab/check.rake#L782)
1. Check the [Git version](/lib/tasks/gitlab/check.rake#L794)
1. There might be other changes. Ask around.
### 2. Create update guides
[Follow this guide](howto_update_guides.md) to create update guides.
### 3. Code quality indicators
Make sure the code quality indicators are green / good.
- [![Build status](http://ci.gitlab.org/projects/1/status.png?ref=master)](http://ci.gitlab.org/projects/1?ref=master) on ci.gitlab.org (master branch)
- [![Build Status](https://semaphoreapp.com/api/v1/projects/2f1a5809-418b-4cc2-a1f4-819607579fe7/243338/badge.png)](https://semaphoreapp.com/gitlabhq/gitlabhq) (master branch)
- [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.png)](https://codeclimate.com/github/gitlabhq/gitlabhq)
- [![Dependency Status](https://gemnasium.com/gitlabhq/gitlabhq.png)](https://gemnasium.com/gitlabhq/gitlabhq) this button can be yellow (small updates are available) but must not be red (a security fix or an important update is available)
- [![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.png?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq)
### 4. Run release tool
**Make sure EE `master` has latest changes from CE `master`**
Get release tools
```
git clone git@dev.gitlab.org:gitlab/release-tools.git
cd release-tools
```
Release candidate creates stable branch from master.
So we need to sync master branch between all CE, EE and CI remotes.
```
bundle exec rake sync
```
Create release candidate and stable branch:
```
bundle exec rake release["x.x.0.rc1"]
```
Now developers can use master for merging new features.
So you should use stable branch for future code changes related to release.
# Create update guides
1. Create: CE update guide from previous version. Like `7.3-to-7.4.md`
1. Create: CE to EE update guide in EE repository for latest version.
1. Update: `6.x-or-7.x-to-7.x.md` to latest version.
1. Create: CI update guide from previous version
It's best to copy paste the previous guide and make changes where necessary.
The typical steps are listed below with any points you should specifically look at.
#### 0. Any major changes?
List any major changes here, so the user is aware of them before starting to upgrade. For instance:
- Database updates
- Web server changes
- File structure changes
#### 1. Stop server
#### 2. Make backup
#### 3. Do users need to update dependencies like `git`?
- Check if the [GitLab Shell version](/lib/tasks/gitlab/check.rake#L782) changed since the last release.
- Check if the [Git version](/lib/tasks/gitlab/check.rake#L794) changed since the last release.
#### 4. Get latest code
#### 5. Does GitLab shell need to be updated?
#### 6. Install libs, migrations, etc.
#### 7. Any config files updated since last release?
Check if any of these changed since last release:
- [lib/support/nginx/gitlab](/lib/support/nginx/gitlab)
- [lib/support/nginx/gitlab-ssl](/lib/support/nginx/gitlab-ssl)
- <https://gitlab.com/gitlab-org/gitlab-shell/commits/master/config.yml.example>
- [config/gitlab.yml.example](/config/gitlab.yml.example)
- [config/unicorn.rb.example](/config/unicorn.rb.example)
- [config/database.yml.mysql](/config/database.yml.mysql)
- [config/database.yml.postgresql](/config/database.yml.postgresql)
- [config/initializers/rack_attack.rb.example](/config/initializers/rack_attack.rb.example)
- [config/resque.yml.example](/config/resque.yml.example)
#### 8. Need to update init script?
Check if the `init.d/gitlab` script changed since last release: [lib/support/init.d/gitlab](/lib/support/init.d/gitlab)
#### 9. Start application
#### 10. Check application status
# How to push GitLab CE master branch to all remotes.
The source code of GitLab is available on multiple servers (with GitLab.com as the canonical source).
Synchronization between the repo's is done by the lead developer if there is no rush.
This happens a few times per workday on average.
If somebody else with access to all repo's wants to do it the instructions are below.
This is just to distribute changes, not to make them.
## Add this to `.bashrc` or [your dotfiles](https://github.com/dosire/dotfiles/commit/52803ce3ac60d57632164b7713ff0041e86fa26c)
```bash
gpa ()
{
git push origin ${1:-master} && git push gh ${1:-master} && git push gl ${1:-master}
}
```
## Then add remotes to your local repo
```bash
cd my-gitlab-ce-repo
git remote add origin git@dev.gitlab.org:gitlab/gitlabhq.git
git remote add gh git@github.com:gitlabhq/gitlabhq.git
git remote add gl git@gitlab.com:gitlab-org/gitlab-ce.git
```
## Push to all remotes
```bash
gpa
```
# Yanking packages from packages.gitlab.com
In case something went wrong with the release and there is a need to remove the packages you can yank the packages by following the
procedure described in [package cloud documentation](https://packagecloud.io/docs#yank_pkg).
You need to have:
1. `package_cloud` gem installed (sudo gem install package_cloud)
1. Email and password for packages.gitlab.com
1. Make sure that you are supplying the url to packages.gitlab.com (default is packagecloud.io)
Example of yanking a package:
```bash
package_cloud yank --url https://packages.gitlab.com gitlab/gitlab-ce/el/6 gitlab-ce-7.10.2~omnibus-1.x86_64.rpm
```
If you are attempting this for the first time the output will look something like:
```bash
Looking for repository at gitlab/gitlab-ce... No config file exists at /Users/marin/.packagecloud. Login to create one.
Email:
marin@gitlab.com
Password:
Got your token. Writing a config file to /Users/marin/.packagecloud... success!
success!
Attempting to yank package at gitlab/gitlab-ce/el/6/gitlab-ce-7.10.2~omnibus-1.x86_64.rpm...done!
```
This diff is collapsed.
# Things to do when doing a patch release
NOTE: This is a guide for GitLab developers. If you are trying to install GitLab
see the latest stable [installation guide](install/installation.md) and if you
are trying to upgrade, see the [upgrade guides](update).
## When to do a patch release
Patch releases are done as-needed in order to fix regressions in the current
major release that cannot or should not wait until the next major release.
What's included and when to release is at the discretion of the release manager.
## Release Procedure
### Create a patch issue
Create an issue in the GitLab CE project. Name it "Release x.y.z", tag it with
the `release` label, and assign it to the milestone of the corresponding major
release.
Use the following template:
```
- Picked into respective `stable` branches:
- [ ] Merge `x-y-stable` into `x-y-stable-ee`
- [ ] release-tools: `x.y.z`
- omnibus-gitlab
- [ ] `x.y.z+ee.0`
- [ ] `x.y.z+ce.0`
- [ ] Deploy
- [ ] Add patch notice to [x.y regressions]()
- [ ] [Blog post]()
- [ ] [Tweet]()
- [ ] Add entry to version.gitlab.com
```
Update the issue with links to merge requests that need to be/have been picked
into the `stable` branches.
### Preparation
1. Verify that the issue can be reproduced
1. Note in the 'GitLab X.X regressions' that you will create a patch
1. Fix the issue on a feature branch, do this on the private GitLab development server
1. If it is a security issue, then assign it to the release manager and apply a 'security' label
1. Consider creating and testing workarounds
1. After the branch is merged into master, cherry pick the commit(s) into the current stable branch
1. Make sure that the build has passed and all tests are passing
1. In a separate commit in the master branch update the CHANGELOG
1. For EE, update the CHANGELOG-EE if it is EE specific fix. Otherwise, merge the stable CE branch and add to CHANGELOG-EE "Merge community edition changes for version X.X.X"
1. Merge CE stable branch into EE stable branch
### Bump version
Get release tools
```
git clone git@dev.gitlab.org:gitlab/release-tools.git
cd release-tools
```
Bump all versions in stable branch, even if the changes affect only EE, CE, or CI. Since all the versions are synced now,
it doesn't make sense to say upgrade CE to 7.2, EE to 7.3 and CI to 7.1.
Create release tag and push to remotes:
```
bundle exec rake release["x.x.x"]
```
## Release
1. [Build new packages with the latest version](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md)
1. Apply the patch to GitLab.com and the private GitLab development server
1. Apply the patch to ci.gitLab.com and the private GitLab CI development server
1. Create and publish a blog post, see [patch release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/patch_release_blog_template.md)
1. Send tweets about the release from `@gitlab`, tweet should include the most important feature that the release is addressing and link to the blog post
1. Note in the 'GitLab X.X regressions' issue that the patch was published (CE only)
1. Create the 'x.y.0' version on version.gitlab.com
1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md)
1. Create a new patch release issue for the next potential release
# Things to do when doing an out-of-bound security release
NOTE: This is a guide for GitLab developers. If you are trying to install GitLab see the latest stable [installation guide](install/installation.md) and if you are trying to upgrade, see the [upgrade guides](update).
## When to do a security release
Do a security release when there is a critical issue that needs to be addresses before the next monthly release. Otherwise include it in the monthly release and note there was a security fix in the release announcement.
## Security vulnerability disclosure
Please report suspected security vulnerabilities in private to <support@gitlab.com>, also see the [disclosure section on the GitLab.com website](https://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities.
## Release Procedure
1. Verify that the issue can be reproduced
1. Acknowledge the issue to the researcher that disclosed it
1. Inform the release manager that there needs to be a security release
1. Do the steps from [patch release document](../release/patch.md), starting with "Create an issue on private GitLab development server"
1. The MR with the security fix should get a 'security' label and be assigned to the release manager
1. Build the package for GitLab.com and do a deploy
1. Build the package for ci.gitLab.com and do a deploy
1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md)
1. Create feature branches for the blog post on GitLab.com and link them from the code branch
1. Merge and publish the blog posts
1. Send tweets about the release from `@gitlabhq`
1. Send out an email to [the community google mailing list](https://groups.google.com/forum/#!forum/gitlabhq)
1. Post a signed copy of our complete announcement to [oss-security](http://www.openwall.com/lists/oss-security/) and request a CVE number. CVE is only needed for bugs that allow someone to own the server (Remote Code Execution) or access to code of projects they are not a member of.
1. Add the security researcher to the [Security Researcher Acknowledgments list](https://about.gitlab.com/vulnerability-acknowledgements/)
1. Thank the security researcher in an email for their cooperation
1. Update the blog post and the CHANGELOG when we receive the CVE number
The timing of the code merge into master should be coordinated in advance.
After the merge we strive to publish the announcements within 60 minutes.
## Blog post template
XXX Security Advisory for GitLab
A recently discovered critical vulnerability in GitLab allows [unauthenticated API access|remote code execution|unauthorized access to repositories|XXX|PICKSOMETHING]. All users should update GitLab and gitlab-shell immediately. We [have|haven't|XXX|PICKSOMETHING|] heard of this vulnerability being actively exploited.
### Version affected
GitLab Community Edition XXX and lower
GitLab Enterprise Edition XXX and lower
### Fixed versions
GitLab Community Edition XXX and up
GitLab Enterprise Edition XXX and up
### Impact
On GitLab installations which use MySQL as their database backend it is possible for an attacker to assume the identity of any existing GitLab user in certain API calls. This attack can be performed by [unauthenticated|authenticated|XXX|PICKSOMETHING] users.
### Workarounds
If you are unable to upgrade you should apply the following patch and restart GitLab.
XXX
### Credit
We want to thank XXX of XXX for the responsible disclosure of this vulnerability.
## Email template
We just announced a security advisory for GitLab at XXX
Please contact us at support@gitlab.com if you have any questions.
## Tweet template
We just announced a security advisory for GitLab at XXX
...@@ -62,7 +62,26 @@ sudo -u git -H git checkout v0.7.1 ...@@ -62,7 +62,26 @@ sudo -u git -H git checkout v0.7.1
sudo -u git -H make sudo -u git -H make
``` ```
### 6. Install libs, migrations, etc. ### 6. Updates for PostgreSQL Users
Starting with 8.6 users using GitLab in combination with PostgreSQL are required
to have the `pg_trgm` extension enabled for all GitLab databases. If you're
using GitLab's Omnibus packages there's nothing you'll need to do manually as
this extension is enabled automatically. Users who install GitLab without using
Omnibus (e.g. by building from source) have to enable this extension manually.
To enable this extension run the following SQL command as a PostgreSQL super
user for _every_ GitLab database:
```sql
CREATE EXTENSION IF NOT EXISTS pg_trgm;
```
Certain operating systems might require the installation of extra packages for
this extension to be available. For example, users using Ubuntu will have to
install the `postgresql-contrib` package in order for this extension to be
available.
### 7. Install libs, migrations, etc.
```bash ```bash
cd /home/git/gitlab cd /home/git/gitlab
...@@ -84,7 +103,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS ...@@ -84,7 +103,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
``` ```
### 7. Update configuration files ### 8. Update configuration files
#### New configuration options for `gitlab.yml` #### New configuration options for `gitlab.yml`
...@@ -120,25 +139,6 @@ Ensure you're still up-to-date with the latest init script changes: ...@@ -120,25 +139,6 @@ Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
### 8. Updates for PostgreSQL Users
Starting with 8.6 users using GitLab in combination with PostgreSQL are required
to have the `pg_trgm` extension enabled for all GitLab databases. If you're
using GitLab's Omnibus packages there's nothing you'll need to do manually as
this extension is enabled automatically. Users who install GitLab without using
Omnibus (e.g. by building from source) have to enable this extension manually.
To enable this extension run the following SQL command as a PostgreSQL super
user for _every_ GitLab database:
```sql
CREATE EXTENSION IF NOT EXISTS pg_trgm;
```
Certain operating systems might require the installation of extra packages for
this extension to be available. For example, users using Ubuntu will have to
install the `postgresql-contrib` package in order for this extension to be
available.
### 9. Start application ### 9. Start application
sudo service gitlab start sudo service gitlab start
......
...@@ -58,13 +58,13 @@ X-Gitlab-Event: Push Hook ...@@ -58,13 +58,13 @@ X-Gitlab-Event: Push Hook
"path_with_namespace":"mike/diaspora", "path_with_namespace":"mike/diaspora",
"default_branch":"master", "default_branch":"master",
"homepage":"http://example.com/mike/diaspora", "homepage":"http://example.com/mike/diaspora",
"url":"git@example.com:mike/diasporadiaspora.git", "url":"git@example.com:mike/diaspora.git",
"ssh_url":"git@example.com:mike/diaspora.git", "ssh_url":"git@example.com:mike/diaspora.git",
"http_url":"http://example.com/mike/diaspora.git" "http_url":"http://example.com/mike/diaspora.git"
}, },
"repository":{ "repository":{
"name": "Diaspora", "name": "Diaspora",
"url": "git@example.com:mike/diasporadiaspora.git", "url": "git@example.com:mike/diaspora.git",
"description": "", "description": "",
"homepage": "http://example.com/mike/diaspora", "homepage": "http://example.com/mike/diaspora",
"git_http_url":"http://example.com/mike/diaspora.git", "git_http_url":"http://example.com/mike/diaspora.git",
...@@ -113,7 +113,6 @@ Triggered when you create (or delete) tags to the repository. ...@@ -113,7 +113,6 @@ Triggered when you create (or delete) tags to the repository.
X-Gitlab-Event: Tag Push Hook X-Gitlab-Event: Tag Push Hook
``` ```
**Request body:** **Request body:**
```json ```json
...@@ -143,7 +142,7 @@ X-Gitlab-Event: Tag Push Hook ...@@ -143,7 +142,7 @@ X-Gitlab-Event: Tag Push Hook
"http_url":"http://example.com/jsmith/example.git" "http_url":"http://example.com/jsmith/example.git"
}, },
"repository":{ "repository":{
"name": "jsmith", "name": "Example",
"url": "ssh://git@example.com/jsmith/example.git", "url": "ssh://git@example.com/jsmith/example.git",
"description": "", "description": "",
"homepage": "http://example.com/jsmith/example", "homepage": "http://example.com/jsmith/example",
...@@ -478,7 +477,7 @@ X-Gitlab-Event: Note Hook ...@@ -478,7 +477,7 @@ X-Gitlab-Event: Note Hook
}, },
"repository":{ "repository":{
"name":"diaspora", "name":"diaspora",
"url":"git@example.com:mike/diasporadiaspora.git", "url":"git@example.com:mike/diaspora.git",
"description":"", "description":"",
"homepage":"http://example.com/mike/diaspora" "homepage":"http://example.com/mike/diaspora"
}, },
......
...@@ -23,6 +23,10 @@ In `/etc/gitlab/gitlab.rb`: ...@@ -23,6 +23,10 @@ In `/etc/gitlab/gitlab.rb`:
```ruby ```ruby
gitlab_rails['lfs_enabled'] = false gitlab_rails['lfs_enabled'] = false
# Optionally, change the storage path location. Defaults to
# `#{gitlab_rails['shared_path']}/lfs-objects`. Which evaluates to
# `/var/opt/gitlab/gitlab-rails/shared/lfs-objects` by default.
gitlab_rails['lfs_storage_path'] = "/mnt/storage/lfs-objects" gitlab_rails['lfs_storage_path'] = "/mnt/storage/lfs-objects"
``` ```
......
...@@ -117,11 +117,15 @@ module API ...@@ -117,11 +117,15 @@ module API
# assignee_id (optional) - The ID of a user to assign issue # assignee_id (optional) - The ID of a user to assign issue
# milestone_id (optional) - The ID of a milestone to assign issue # milestone_id (optional) - The ID of a milestone to assign issue
# labels (optional) - The labels of an issue # labels (optional) - The labels of an issue
# created_at (optional) - The date
# Example Request: # Example Request:
# POST /projects/:id/issues # POST /projects/:id/issues
post ":id/issues" do post ":id/issues" do
required_attributes! [:title] required_attributes! [:title]
attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id]
keys = [:title, :description, :assignee_id, :milestone_id]
keys << :created_at if current_user.admin? || user_project.owner == current_user
attrs = attributes_for_keys(keys)
# Validate label names in advance # Validate label names in advance
if (errors = validate_label_params(params)).any? if (errors = validate_label_params(params)).any?
......
...@@ -5,6 +5,27 @@ describe Ci::ProjectsController do ...@@ -5,6 +5,27 @@ describe Ci::ProjectsController do
let!(:project) { create(:project, visibility, ci_id: 1) } let!(:project) { create(:project, visibility, ci_id: 1) }
let(:ci_id) { project.ci_id } let(:ci_id) { project.ci_id }
describe '#index' do
context 'user signed in' do
before do
sign_in(create(:user))
get(:index)
end
it 'redirects to /' do
expect(response).to redirect_to(root_path)
end
end
context 'user not signed in' do
before { get(:index) }
it 'redirects to sign in page' do
expect(response).to redirect_to(new_user_session_path)
end
end
end
## ##
# Specs for *deprecated* CI badge # Specs for *deprecated* CI badge
# #
......
require 'spec_helper'
describe Projects::SnippetsController do
let(:project) { create(:project_empty_repo, :public, snippets_enabled: true) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
before do
project.team << [user, :master]
project.team << [user2, :master]
end
describe 'GET #index' do
context 'when the project snippet is private' do
let!(:project_snippet) { create(:project_snippet, :private, project: project, author: user) }
context 'when anonymous' do
it 'does not include the private snippet' do
get :index, namespace_id: project.namespace.path, project_id: project.path
expect(assigns(:snippets)).not_to include(project_snippet)
expect(response.status).to eq(200)
end
end
context 'when signed in as the author' do
before { sign_in(user) }
it 'renders the snippet' do
get :index, namespace_id: project.namespace.path, project_id: project.path
expect(assigns(:snippets)).to include(project_snippet)
expect(response.status).to eq(200)
end
end
context 'when signed in as a project member' do
before { sign_in(user2) }
it 'renders the snippet' do
get :index, namespace_id: project.namespace.path, project_id: project.path
expect(assigns(:snippets)).to include(project_snippet)
expect(response.status).to eq(200)
end
end
end
end
%w[show raw].each do |action|
describe "GET ##{action}" do
context 'when the project snippet is private' do
let(:project_snippet) { create(:project_snippet, :private, project: project, author: user) }
context 'when anonymous' do
it 'responds with status 404' do
get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param
expect(response.status).to eq(404)
end
end
context 'when signed in as the author' do
before { sign_in(user) }
it 'renders the snippet' do
get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param
expect(assigns(:snippet)).to eq(project_snippet)
expect(response.status).to eq(200)
end
end
context 'when signed in as a project member' do
before { sign_in(user2) }
it 'renders the snippet' do
get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param
expect(assigns(:snippet)).to eq(project_snippet)
expect(response.status).to eq(200)
end
end
end
context 'when the project snippet does not exist' do
context 'when anonymous' do
it 'responds with status 404' do
get action, namespace_id: project.namespace.path, project_id: project.path, id: 42
expect(response.status).to eq(404)
end
end
context 'when signed in' do
before { sign_in(user) }
it 'responds with status 404' do
get action, namespace_id: project.namespace.path, project_id: project.path, id: 42
expect(response.status).to eq(404)
end
end
end
end
end
end
...@@ -43,6 +43,28 @@ describe RootController do ...@@ -43,6 +43,28 @@ describe RootController do
end end
end end
context 'who has customized their dashboard setting for groups' do
before do
user.update_attribute(:dashboard, 'groups')
end
it 'redirects to their group list' do
get :index
expect(response).to redirect_to dashboard_groups_path
end
end
context 'who has customized their dashboard setting for todos' do
before do
user.update_attribute(:dashboard, 'todos')
end
it 'redirects to their todo list' do
get :index
expect(response).to redirect_to dashboard_todos_path
end
end
context 'who uses the default dashboard setting' do context 'who uses the default dashboard setting' do
it 'renders the default dashboard' do it 'renders the default dashboard' do
get :index get :index
......
...@@ -31,7 +31,7 @@ feature 'Issue filtering by Milestone', feature: true do ...@@ -31,7 +31,7 @@ feature 'Issue filtering by Milestone', feature: true do
def filter_by_milestone(title) def filter_by_milestone(title)
find(".js-milestone-select").click find(".js-milestone-select").click
sleep 0.5 sleep 0.5
find(".milestone-filter a", text: title).click find(".milestone-filter .dropdown-content a", text: title).click
sleep 1 sleep 1
end end
end end
...@@ -34,22 +34,9 @@ describe 'Issues', feature: true do ...@@ -34,22 +34,9 @@ describe 'Issues', feature: true do
fill_in 'issue_title', with: 'bug 345' fill_in 'issue_title', with: 'bug 345'
fill_in 'issue_description', with: 'bug description' fill_in 'issue_description', with: 'bug description'
end end
it 'does not change issue count' do
expect { click_button 'Save changes' }.to_not change { Issue.count }
end
it 'should update issue fields' do
click_button 'Save changes'
expect(page).to have_content @user.name
expect(page).to have_content 'bug 345'
expect(page).to have_content project.name
end end
end end
end
describe 'Editing issue assignee' do describe 'Editing issue assignee' do
let!(:issue) do let!(:issue) do
create(:issue, create(:issue,
...@@ -70,7 +57,7 @@ describe 'Issues', feature: true do ...@@ -70,7 +57,7 @@ describe 'Issues', feature: true do
click_button 'Save changes' click_button 'Save changes'
page.within('.assignee') do page.within('.assignee') do
expect(page).to have_content 'None' expect(page).to have_content 'No assignee - assign yourself'
end end
expect(issue.reload.assignee).to be_nil expect(issue.reload.assignee).to be_nil
...@@ -198,20 +185,26 @@ describe 'Issues', feature: true do ...@@ -198,20 +185,26 @@ describe 'Issues', feature: true do
end end
describe 'update assignee from issue#show' do describe 'update assignee from issue#show' do
let(:issue) { create(:issue, project: project, author: @user) } let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
context 'by autorized user' do context 'by autorized user' do
it 'with dropdown menu' do it 'allows user to select unassigned', js: true do
visit namespace_project_issue_path(project.namespace, project, issue) visit namespace_project_issue_path(project.namespace, project, issue)
find('.issuable-sidebar #issue_assignee_id'). page.within('.assignee') do
set project.team.members.first.id expect(page).to have_content "#{@user.name}"
click_button 'Update Issue' end
find('.block.assignee .edit-link').click
sleep 2 # wait for ajax stuff to complete
first('.dropdown-menu-user-link').click
sleep 2
page.within('.assignee') do
expect(page).to have_content 'No assignee'
end
expect(page).to have_content 'Assignee' expect(issue.reload.assignee).to be_nil
has_select?('issue_assignee_id',
selected: project.team.members.first.name)
end end
end end
...@@ -221,8 +214,6 @@ describe 'Issues', feature: true do ...@@ -221,8 +214,6 @@ describe 'Issues', feature: true do
before :each do before :each do
project.team << [[guest], :guest] project.team << [[guest], :guest]
issue.assignee = @user
issue.save
end end
it 'shows assignee text', js: true do it 'shows assignee text', js: true do
...@@ -241,20 +232,23 @@ describe 'Issues', feature: true do ...@@ -241,20 +232,23 @@ describe 'Issues', feature: true do
context 'by authorized user' do context 'by authorized user' do
it 'with dropdown menu' do
visit namespace_project_issue_path(project.namespace, project, issue)
find('.issuable-sidebar'). it 'allows user to select unassigned', js: true do
select(milestone.title, from: 'issue_milestone_id') visit namespace_project_issue_path(project.namespace, project, issue)
click_button 'Update Issue'
expect(page).to have_content "Milestone changed to #{milestone.title}" page.within('.milestone') do
expect(page).to have_content "None"
end
find('.block.milestone .edit-link').click
sleep 2 # wait for ajax stuff to complete
first('.dropdown-content li').click
sleep 2
page.within('.milestone') do page.within('.milestone') do
expect(page).to have_content milestone.title expect(page).to have_content 'None'
end end
has_select?('issue_assignee_id', selected: milestone.title) expect(issue.reload.milestone).to be_nil
end end
end end
...@@ -283,25 +277,6 @@ describe 'Issues', feature: true do ...@@ -283,25 +277,6 @@ describe 'Issues', feature: true do
issue.assignee = user2 issue.assignee = user2
issue.save issue.save
end end
it 'allows user to remove assignee', js: true do
visit namespace_project_issue_path(project.namespace, project, issue)
page.within('.assignee') do
expect(page).to have_content user2.name
end
find('.assignee .edit-link').click
sleep 2 # wait for ajax stuff to complete
first('.user-result').click
page.within('.assignee') do
expect(page).to have_content 'None'
end
sleep 2 # wait for ajax stuff to complete
expect(issue.reload.assignee).to be_nil
end
end end
end end
......
require 'spec_helper'
describe "Internal Project Snippets Access", feature: true do
include AccessMatchers
let(:project) { create(:project, :internal) }
let(:owner) { project.owner }
let(:master) { create(:user) }
let(:developer) { create(:user) }
let(:reporter) { create(:user) }
let(:guest) { create(:user) }
let(:internal_snippet) { create(:project_snippet, :internal, project: project, author: owner) }
let(:private_snippet) { create(:project_snippet, :private, project: project, author: owner) }
before do
project.team << [master, :master]
project.team << [developer, :developer]
project.team << [reporter, :reporter]
project.team << [guest, :guest]
end
describe "GET /:project_path/snippets" do
subject { namespace_project_snippets_path(project.namespace, project) }
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/snippets/new" do
subject { new_namespace_project_snippet_path(project.namespace, project) }
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/snippets/:id for an internal snippet" do
subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) }
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/snippets/:id for a private snippet" do
subject { namespace_project_snippet_path(project.namespace, project, private_snippet) }
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
end
require 'spec_helper'
describe "Private Project Snippets Access", feature: true do
include AccessMatchers
let(:project) { create(:project, :private) }
let(:owner) { project.owner }
let(:master) { create(:user) }
let(:developer) { create(:user) }
let(:reporter) { create(:user) }
let(:guest) { create(:user) }
let(:private_snippet) { create(:project_snippet, :private, project: project, author: owner) }
before do
project.team << [master, :master]
project.team << [developer, :developer]
project.team << [reporter, :reporter]
project.team << [guest, :guest]
end
describe "GET /:project_path/snippets" do
subject { namespace_project_snippets_path(project.namespace, project) }
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/snippets/new" do
subject { new_namespace_project_snippet_path(project.namespace, project) }
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/snippets/:id for a private snippet" do
subject { namespace_project_snippet_path(project.namespace, project, private_snippet) }
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
end
require 'spec_helper'
describe "Public Project Snippets Access", feature: true do
include AccessMatchers
let(:project) { create(:project, :public) }
let(:owner) { project.owner }
let(:master) { create(:user) }
let(:developer) { create(:user) }
let(:reporter) { create(:user) }
let(:guest) { create(:user) }
let(:public_snippet) { create(:project_snippet, :public, project: project, author: owner) }
let(:internal_snippet) { create(:project_snippet, :internal, project: project, author: owner) }
let(:private_snippet) { create(:project_snippet, :private, project: project, author: owner) }
before do
project.team << [master, :master]
project.team << [developer, :developer]
project.team << [reporter, :reporter]
project.team << [guest, :guest]
end
describe "GET /:project_path/snippets" do
subject { namespace_project_snippets_path(project.namespace, project) }
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_allowed_for :external }
it { is_expected.to be_allowed_for :visitor }
end
describe "GET /:project_path/snippets/new" do
subject { new_namespace_project_snippet_path(project.namespace, project) }
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
it { is_expected.to be_denied_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/snippets/:id for a public snippet" do
subject { namespace_project_snippet_path(project.namespace, project, public_snippet) }
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_allowed_for :external }
it { is_expected.to be_allowed_for :visitor }
end
describe "GET /:project_path/snippets/:id for an internal snippet" do
subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) }
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
describe "GET /:project_path/snippets/:id for a private snippet" do
subject { namespace_project_snippet_path(project.namespace, project, private_snippet) }
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
it { is_expected.to be_allowed_for developer }
it { is_expected.to be_allowed_for reporter }
it { is_expected.to be_allowed_for guest }
it { is_expected.to be_denied_for :user }
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
end
...@@ -19,7 +19,9 @@ describe PreferencesHelper do ...@@ -19,7 +19,9 @@ describe PreferencesHelper do
['Your Projects (default)', 'projects'], ['Your Projects (default)', 'projects'],
['Starred Projects', 'stars'], ['Starred Projects', 'stars'],
["Your Projects' Activity", 'project_activity'], ["Your Projects' Activity", 'project_activity'],
["Starred Projects' Activity", 'starred_project_activity'] ["Starred Projects' Activity", 'starred_project_activity'],
["Your Groups", 'groups'],
["Your Todos", 'todos']
] ]
end end
end end
......
...@@ -422,6 +422,12 @@ describe Project, models: true do ...@@ -422,6 +422,12 @@ describe Project, models: true do
it { should eq "http://localhost#{avatar_path}" } it { should eq "http://localhost#{avatar_path}" }
end end
context 'when git repo is empty' do
let(:project) { create(:empty_project) }
it { should eq nil }
end
end end
describe :ci_commit do describe :ci_commit do
......
...@@ -2,6 +2,7 @@ require 'spec_helper' ...@@ -2,6 +2,7 @@ require 'spec_helper'
describe Repository, models: true do describe Repository, models: true do
include RepoHelpers include RepoHelpers
TestBlob = Struct.new(:name)
let(:repository) { create(:project).repository } let(:repository) { create(:project).repository }
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -131,7 +132,6 @@ describe Repository, models: true do ...@@ -131,7 +132,6 @@ describe Repository, models: true do
describe "#license" do describe "#license" do
before do before do
repository.send(:cache).expire(:license) repository.send(:cache).expire(:license)
TestBlob = Struct.new(:name)
end end
it 'test selection preference' do it 'test selection preference' do
...@@ -148,6 +148,25 @@ describe Repository, models: true do ...@@ -148,6 +148,25 @@ describe Repository, models: true do
end end
end end
describe "#gitlab_ci_yml" do
it 'returns valid file' do
files = [TestBlob.new('file'), TestBlob.new('.gitlab-ci.yml'), TestBlob.new('copying')]
expect(repository.tree).to receive(:blobs).and_return(files)
expect(repository.gitlab_ci_yml.name).to eq('.gitlab-ci.yml')
end
it 'returns nil if not exists' do
expect(repository.tree).to receive(:blobs).and_return([])
expect(repository.gitlab_ci_yml).to be_nil
end
it 'returns nil for empty repository' do
expect(repository).to receive(:empty?).and_return(true)
expect(repository.gitlab_ci_yml).to be_nil
end
end
describe :add_branch do describe :add_branch do
context 'when pre hooks were successful' do context 'when pre hooks were successful' do
it 'should run without errors' do it 'should run without errors' do
...@@ -725,6 +744,12 @@ describe Repository, models: true do ...@@ -725,6 +744,12 @@ describe Repository, models: true do
end end
describe '#avatar' do describe '#avatar' do
it 'returns nil if repo does not exist' do
expect(repository).to receive(:exists?).and_return(false)
expect(repository.avatar).to eq(nil)
end
it 'returns the first avatar file found in the repository' do it 'returns the first avatar file found in the repository' do
expect(repository).to receive(:blob_at_branch). expect(repository).to receive(:blob_at_branch).
with('master', 'logo.png'). with('master', 'logo.png').
......
...@@ -318,6 +318,17 @@ describe API::API, api: true do ...@@ -318,6 +318,17 @@ describe API::API, api: true do
'is too long (maximum is 255 characters)' 'is too long (maximum is 255 characters)'
]) ])
end end
context 'when an admin or owner makes the request' do
it "accepts the creation date to be set" do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', created_at: 2.weeks.ago
expect(response.status).to eq(201)
# this take about a second, so probably not equal
expect(Time.parse(json_response['created_at'])).to be <= 2.weeks.ago
end
end
end end
describe 'POST /projects/:id/issues with spam filtering' do describe 'POST /projects/:id/issues with spam filtering' do
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment