Commit 5c81fc43 authored by Alfredo Sumaran's avatar Alfredo Sumaran

Merge branch 'master' into issue_14678

# Conflicts:
#	app/assets/javascripts/todos.js.coffee
parents 5c307cf1 79467437
...@@ -4,14 +4,37 @@ v 8.7.0 (unreleased) ...@@ -4,14 +4,37 @@ v 8.7.0 (unreleased)
- Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu) - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu)
- 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 - Expose label description in API (Mariusz Jachimowicz)
- 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
- Fix raw/rendered diff producing different results on merge requests !3450
- Add links to CI setup documentation from project settings and builds pages - Add links to CI setup documentation from project settings and builds pages
- Handle nil descriptions in Slack issue messages (Stan Hu)
- Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.) - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
- Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.) - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
- Gracefully handle notes on deleted commits in merge requests (Stan Hu)
v 8.6.2 (unreleased)
- Comments on confidential issues don't show up in activity feed to non-members v 8.6.2
- Fix dropdown alignment. !3298
- Fix issuable sidebar overlaps on tablet. !3299
- Make dropdowns pixel perfect. !3337
- Fix order of steps to prevent PostgreSQL errors when running migration. !3355
- Fix bold text in issuable sidebar. !3358
- Fix error with anonymous token in applications settings. !3362
- Fix the milestone 'upcoming' filter. !3364 + !3368
- Fix comments on confidential issues showing up in activity feed to non-members. !3375
- Fix `NoMethodError` when visiting CI root path at `/ci`. !3377
- Add a tooltip to new branch button in issue page. !3380
- Fix an issue hiding the password form when signed-in with a linked account. !3381
- Add links to CI setup documentation from project settings and builds pages. !3384
- Fix an issue with width of project select dropdown. !3386
- Remove redundant `require`s from Banzai files. !3391
- Fix error 500 with cancel button on issuable edit form. !3392 + !3417
- Fix background when editing a highlighted note. !3423
- Remove tabstop from the WIP toggle links. !3426
- Ensure private project snippets are not viewable by unauthorized people.
- Gracefully handle notes on deleted commits in merge requests (Stan Hu). !3402
- Fixed issue with notification settings not saving. !3452
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
...@@ -51,6 +74,7 @@ v 8.6.0 ...@@ -51,6 +74,7 @@ v 8.6.0
- Fix wiki search results point to raw source (Hiroyuki Sato) - Fix wiki search results point to raw source (Hiroyuki Sato)
- Don't load all of GitLab in mail_room - Don't load all of GitLab in mail_room
- Add information about `image` and `services` field at `job` level in the `.gitlab-ci.yml` documentation (Pat Turner) - Add information about `image` and `services` field at `job` level in the `.gitlab-ci.yml` documentation (Pat Turner)
- Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla)
- HTTP error pages work independently from location and config (Artem Sidorenko) - HTTP error pages work independently from location and config (Artem Sidorenko)
- Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set - Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set
- Memoize @group in Admin::GroupsController (Yatish Mehta) - Memoize @group in Admin::GroupsController (Yatish Mehta)
......
...@@ -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,10 @@ class GitLabDropdown ...@@ -143,10 +145,10 @@ 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) ->
self.rowClicked $(@) selected = self.rowClicked $(@)
if self.options.clicked if self.options.clicked
self.options.clicked() self.options.clicked(selected)
toggleLoading: -> toggleLoading: ->
$('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
...@@ -176,6 +178,15 @@ class GitLabDropdown ...@@ -176,6 +178,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 +195,9 @@ class GitLabDropdown ...@@ -184,7 +195,9 @@ class GitLabDropdown
if @options.filterable if @options.filterable
@dropdown.find(".dropdown-input-field").focus() @dropdown.find(".dropdown-input-field").focus()
hidden: => @dropdown.trigger('shown.gl.dropdown')
hidden: (e) =>
if @options.filterable if @options.filterable
@dropdown @dropdown
.find(".dropdown-input-field") .find(".dropdown-input-field")
...@@ -195,6 +208,11 @@ class GitLabDropdown ...@@ -195,6 +208,11 @@ 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)
@dropdown.trigger('hidden.gl.dropdown')
# Render the full menu # Render the full menu
renderMenu: (html) -> renderMenu: (html) ->
...@@ -226,6 +244,13 @@ class GitLabDropdown ...@@ -226,6 +244,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,41 +283,43 @@ class GitLabDropdown ...@@ -258,41 +283,43 @@ 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
fieldName = @options.fieldName
selectedIndex = el.parent().index()
if @renderedData
selectedObject = @renderedData[selectedIndex]
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
# Toggle the dropdown label
if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel
else
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.addClass ACTIVE_CLASS
# 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 return selectedObject
selectFirstRow: -> selectFirstRow: ->
selector = '.dropdown-content li:first-child a' selector = '.dropdown-content li:first-child a'
......
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", ->
...@@ -10,11 +9,21 @@ class @IssuableContext ...@@ -10,11 +9,21 @@ class @IssuableContext
$(".issuable-sidebar .inline-update").on "change", ".js-assignee", -> $(".issuable-sidebar .inline-update").on "change", ".js-assignee", ->
$(this).submit() $(this).submit()
$(document).on "click",".edit-link", (e) -> $(document).off("click", ".edit-link").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()
......
@Issues = @Issues =
init: -> init: ->
Issues.initSearch() Issues.initSearch()
Issues.initSelects()
Issues.initChecks() Issues.initChecks()
$("body").on "ajax:success", ".close_issue, .reopen_issue", -> $("body").on "ajax:success", ".close_issue, .reopen_issue", ->
...@@ -17,18 +16,9 @@ ...@@ -17,18 +16,9 @@
$(this).html totalIssues - 1 $(this).html totalIssues - 1
reload: -> reload: ->
Issues.initSelects()
Issues.initChecks() Issues.initChecks()
$('#filter_issue_search').val($('#issue_search').val()) $('#filter_issue_search').val($('#issue_search').val())
initSelects: ->
$("select#update_state_event").select2(width: 'resolve', dropdownAutoWidth: true)
$("select#update_assignee_id").select2(width: 'resolve', dropdownAutoWidth: true)
$("select#update_milestone_id").select2(width: 'resolve', dropdownAutoWidth: true)
$("select#label_name").select2(width: 'resolve', dropdownAutoWidth: true)
$("#milestone_id, #assignee_id, #label_name").on "change", ->
$(this).closest("form").submit()
initChecks: -> initChecks: ->
$(".check_all_issues").click -> $(".check_all_issues").click ->
$(".selected_issue").prop("checked", @checked) $(".selected_issue").prop("checked", @checked)
......
...@@ -4,14 +4,21 @@ class @LabelsSelect ...@@ -4,14 +4,21 @@ 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')
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span')
$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 +28,22 @@ class @LabelsSelect ...@@ -21,6 +28,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 +80,23 @@ class @LabelsSelect ...@@ -57,6 +80,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,41 +130,84 @@ class @LabelsSelect ...@@ -90,41 +130,84 @@ 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()
$dropdown.trigger('loading.gl.dropdown')
$.ajax(
type: 'PUT'
url: issueUpdateURL
dataType: 'JSON'
data: data
).done (data) ->
$loading.fadeOut()
$dropdown.trigger('loaded.gl.dropdown')
$selectbox.hide()
data.issueURLSplit = issueURLSplit
labelCount = 0
if data.labels.length
template = labelHTMLTemplate(data)
labelCount = data.labels.length
else
template = labelNoneHTMLTemplate()
$value
.removeAttr('style')
.html(template)
$sidebarCollapsedValue.text(labelCount)
$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 showNo if $dropdown.hasClass 'js-extra-options'
data.unshift( if showNo
id: 0 data.unshift(
title: 'No Label' id: 0
) title: 'No Label'
)
if showAny if showAny
data.unshift( data.unshift(
isAny: true isAny: true
title: 'Any Label' title: 'Any Label'
) )
if data.length > 2
data.splice 2, 0, 'divider'
if data.length > 2
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 +216,7 @@ class @LabelsSelect ...@@ -133,6 +216,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,15 +226,33 @@ class @LabelsSelect ...@@ -142,15 +226,33 @@ class @LabelsSelect
id: (label) -> id: (label) ->
if label.isAny? if label.isAny?
'' ''
else else if $dropdown.hasClass "js-filter-submit"
label.title label.title
clicked: -> else
label.id
hidden: ->
$selectbox.hide()
# display:block overrides the hide-collapse rule
$value.removeAttr('style')
if $dropdown.hasClass 'js-multiselect'
saveLabelData()
multiSelect: $dropdown.hasClass 'js-multiselect'
clicked: (label) ->
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-submit') and (isIssueIndex or isMRIndex) if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
selectedLabel = label.title
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')
showUpcoming = $dropdown.data('show-upcoming')
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')
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon')
$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) ->
extraOptions = []
if showAny
extraOptions.push(
id: 0
name: ''
title: 'Any Milestone'
)
if showNo if showNo
data.unshift( extraOptions.push(
id: '0' id: -1
name: 'No Milestone'
title: 'No Milestone' title: 'No Milestone'
) )
if showAny if showUpcoming
data.unshift( extraOptions.push(
isAny: true id: -2
title: 'Any Milestone' name: '#upcoming'
title: 'Upcoming'
) )
if data.length > 2 if extraOptions.length > 2
data.splice 2, 0, 'divider' extraOptions.push 'divider'
callback(data) callback(extraOptions.concat(data))
filterable: true filterable: true
search: search:
fields: ['title'] fields: ['title']
...@@ -45,21 +74,47 @@ class @MilestoneSelect ...@@ -45,21 +74,47 @@ class @MilestoneSelect
milestone.title milestone.title
id: (milestone) -> id: (milestone) ->
if !useId if !useId
if !milestone.isAny? milestone.name
milestone.title
else
''
else else
milestone.id milestone.id
isSelected: (milestone) -> isSelected: (milestone) ->
milestone.title is selectedMilestone milestone.name is selectedMilestone
clicked: -> hidden: ->
page = $('body').data 'page' $selectbox.hide()
isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is page is 'projects:merge_requests:index'
if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) # display:block overrides the hide-collapse rule
Issues.filterResults $dropdown.closest('form') $value.removeAttr('style')
else if $dropdown.hasClass 'js-filter-submit' clicked: (e) ->
$dropdown.closest('form').submit() if $dropdown.hasClass 'js-filter-bulk-update'
) return
if $dropdown.hasClass 'js-filter-submit'
$dropdown.parents('form').submit()
else
selected = $selectbox
.find('input[type="hidden"]')
.val()
data = {}
data[abilityName] = {}
data[abilityName].milestone_id = selected
$loading
.fadeIn()
$dropdown.trigger('loading.gl.dropdown')
$.ajax(
type: 'PUT'
url: issueUpdateURL
data: data
).done (data) ->
$dropdown.trigger('loaded.gl.dropdown')
$loading.fadeOut()
$selectbox.hide()
$value.removeAttr('style')
if data.milestone?
data.milestone.namespace = _this.currentProject.namespace
data.milestone.path = _this.currentProject.path
$value.html(milestoneLinkTemplate(data.milestone))
$sidebarCollapsedValue.find('span').text(data.milestone.title)
else
$value.html(milestoneLinkNoneTemplate)
$sidebarCollapsedValue.find('span').text('No')
)
\ No newline at end of file
class @Sidebar
constructor: (currentUser) ->
@addEventListeners()
addEventListeners: ->
$('aside').on('click', '.sidebar-collapsed-icon', @sidebarCollapseClicked)
$('.dropdown').on('hidden.gl.dropdown', @sidebarDropdownHidden)
$('.dropdown').on('loading.gl.dropdown', @sidebarDropdownLoading)
$('.dropdown').on('loaded.gl.dropdown', @sidebarDropdownLoaded)
sidebarDropdownLoading: (e) ->
$sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
img = $sidebarCollapsedIcon.find('img')
i = $sidebarCollapsedIcon.find('i')
$loading = $('<i class="fa fa-spinner fa-spin"></i>')
if img.length
img.before($loading)
img.hide()
else if i.length
i.before($loading)
i.hide()
sidebarDropdownLoaded: (e) ->
$sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
img = $sidebarCollapsedIcon.find('img')
$sidebarCollapsedIcon.find('i.fa-spin').remove()
i = $sidebarCollapsedIcon.find('i')
if img.length
img.show()
else
i.show()
sidebarCollapseClicked: (e) ->
e.preventDefault()
$block = $(@).closest('.block')
$('aside')
.find('.gutter-toggle')
.trigger('click')
$editLink = $block.find('.edit-link')
if $editLink.length
$editLink.trigger('click')
$block.addClass('collapse-after-update')
$('.page-with-sidebar').addClass('with-overlay')
sidebarDropdownHidden: (e) ->
$block = $(@).closest('.block')
if $block.hasClass('collapse-after-update')
$block.removeClass('collapse-after-update')
$('.page-with-sidebar').removeClass('with-overlay')
$('aside')
.find('.gutter-toggle')
.trigger('click')
\ No newline at end of file
...@@ -8,10 +8,12 @@ class @Todos ...@@ -8,10 +8,12 @@ class @Todos
clearListeners: -> clearListeners: ->
$('.done-todo').off('click') $('.done-todo').off('click')
$('.js-todos-mark-all').off('click') $('.js-todos-mark-all').off('click')
$('.todo').off('click')
initBtnListeners: -> initBtnListeners: ->
$('.done-todo').on('click', @doneClicked) $('.done-todo').on('click', @doneClicked)
$('.js-todos-mark-all').on('click', @allDoneClicked) $('.js-todos-mark-all').on('click', @allDoneClicked)
$('.todo').on('click', @goToTodoUrl)
doneClicked: (e) => doneClicked: (e) =>
e.preventDefault() e.preventDefault()
...@@ -97,3 +99,6 @@ class @Todos ...@@ -97,3 +99,6 @@ class @Todos
return uri.replace(regex, '$1' + key + '=' + value + '$2') return uri.replace(regex, '$1' + key + '=' + value + '$2')
uri + separator + key + '=' + value uri + separator + key + '=' + value
goToTodoUrl: ->
Turbolinks.visit($(this).data('url'))
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,81 @@ class @UsersSelect ...@@ -12,6 +14,81 @@ 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')
$collapsedSidebar = $block.find('.sidebar-collapsed-user')
$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()
$dropdown.trigger('loading.gl.dropdown')
$.ajax(
type: 'PUT'
dataType: 'json'
url: issueURL
data: data
).done (data) ->
$dropdown.trigger('loaded.gl.dropdown')
$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(assigneeTemplate(user))
$collapsedSidebar.html(collapsedAssigneeTemplate(user))
collapsedAssigneeTemplate = _.template(
'<% if( avatar ) { %>
<a class="author_link" href="/u/<%= username %>">
<img width="24" class="avatar avatar-inline s24" alt="" src="<%= avatar %>">
<span class="author">Toni Boehm</span>
</a>
<% } else { %>
<i class="fa fa-user"></i>
<% } %>'
)
assigneeTemplate = _.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 +134,38 @@ class @UsersSelect ...@@ -57,20 +134,38 @@ 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
clicked: ->
inputId: 'issue_assignee_id'
hidden: (e) ->
$selectbox.hide()
# display:block overrides the hide-collapse rule
$value.removeAttr('style')
clicked: (user) ->
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)
selectedId = user.id
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 +182,25 @@ class @UsersSelect ...@@ -87,17 +182,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
<a href='#' class='dropdown-menu-user-link #{selected}'> listWithName = "<li>
#{img} <a href='#' class='dropdown-menu-user-link #{selected}'>
<strong class='dropdown-menu-user-full-name'> #{img}
#{user.name} <strong class='dropdown-menu-user-full-name'>
</strong> #{user.name}
<span class='dropdown-menu-user-username'> </strong>"
#{username}
</span> listWithUserName = "<span class='dropdown-menu-user-username'>
</a> #{username}
</li>" </span>"
listClosingTags = "</a>
</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;
......
...@@ -288,6 +288,10 @@ ...@@ -288,6 +288,10 @@
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
padding-right: $sidebar_collapsed_width; padding-right: $sidebar_collapsed_width;
} }
.sidebar-collapsed-icon {
cursor: pointer;
}
} }
.right-sidebar-expanded { .right-sidebar-expanded {
...@@ -300,4 +304,8 @@ ...@@ -300,4 +304,8 @@
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
padding-right: $gutter_width; padding-right: $gutter_width;
} }
&.with-overlay {
padding-right: $sidebar_collapsed_width;
}
} }
...@@ -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;
} }
} }
......
...@@ -13,6 +13,12 @@ ...@@ -13,6 +13,12 @@
} }
} }
.todo {
&:hover {
cursor: pointer;
}
}
.todo-item { .todo-item {
.todo-title { .todo-title {
@include str-truncated(calc(100% - 174px)); @include str-truncated(calc(100% - 174px));
......
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
...@@ -2,11 +2,12 @@ class Projects::BadgesController < Projects::ApplicationController ...@@ -2,11 +2,12 @@ class Projects::BadgesController < Projects::ApplicationController
before_action :no_cache_headers before_action :no_cache_headers
def build def build
badge = Gitlab::Badge::Build.new(project, params[:ref])
respond_to do |format| respond_to do |format|
format.html { render_404 } format.html { render_404 }
format.svg do format.svg do
image = Ci::ImageForBuildService.new.execute(project, ref: params[:ref]) send_data(badge.data, type: badge.type, disposition: 'inline')
send_file(image.path, filename: image.name, disposition: 'inline', type: 'image/svg+xml')
end 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
......
...@@ -57,8 +57,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -57,8 +57,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json { render json: @merge_request } format.json { render json: @merge_request }
format.diff { render text: @merge_request.to_diff(current_user) } format.diff { render text: @merge_request.to_diff }
format.patch { render text: @merge_request.to_patch(current_user) } format.patch { render text: @merge_request.to_patch }
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: { methods: :avatar_url }])
saved: @merge_request.valid?,
assignee_avatar_url: @merge_request.assignee.try(:avatar_url)
}
end end
end end
else else
......
...@@ -19,13 +19,12 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -19,13 +19,12 @@ 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])
end end
format.json do format.json do
render json: @milestones render json: @milestones.to_json(methods: :name)
end end
end end
end end
......
...@@ -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
......
...@@ -138,7 +138,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -138,7 +138,7 @@ class ProjectsController < Projects::ApplicationController
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id) participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
@suggestions = { @suggestions = {
emojis: autocomplete_emojis, emojis: AwardEmoji.urls,
issues: autocomplete.issues, issues: autocomplete.issues,
mergerequests: autocomplete.merge_requests, mergerequests: autocomplete.merge_requests,
members: participants members: participants
...@@ -235,17 +235,6 @@ class ProjectsController < Projects::ApplicationController ...@@ -235,17 +235,6 @@ class ProjectsController < Projects::ApplicationController
) )
end end
def autocomplete_emojis
Rails.cache.fetch("autocomplete-emoji-#{Gemojione::VERSION}") do
Emoji.emojis.map do |name, emoji|
{
name: name,
path: view_context.image_url("#{emoji["unicode"]}.png")
}
end
end
end
def repo_exists? def repo_exists?
project.repository_exists? && !project.empty_repo? project.repository_exists? && !project.empty_repo?
end end
......
...@@ -243,7 +243,7 @@ class IssuableFinder ...@@ -243,7 +243,7 @@ class IssuableFinder
end end
def filter_by_upcoming_milestone? def filter_by_upcoming_milestone?
params[:milestone_title] == '#upcoming' params[:milestone_title] == Milestone::Upcoming.name
end end
def by_milestone(items) def by_milestone(items)
...@@ -252,7 +252,7 @@ class IssuableFinder ...@@ -252,7 +252,7 @@ class IssuableFinder
items = items.where(milestone_id: [-1, nil]) items = items.where(milestone_id: [-1, nil])
elsif filter_by_upcoming_milestone? elsif filter_by_upcoming_milestone?
upcoming = Milestone.where(project_id: projects).upcoming upcoming = Milestone.where(project_id: projects).upcoming
items = items.joins(:milestone).where(milestones: { title: upcoming.title }) items = items.joins(:milestone).where(milestones: { title: upcoming.try(:title) })
else else
items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] }) items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] })
......
...@@ -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
...@@ -37,6 +47,14 @@ module IssuablesHelper ...@@ -37,6 +47,14 @@ module IssuablesHelper
end end
end end
def milestone_dropdown_label(milestone_title, default_label = "Milestone")
if milestone_title == Milestone::Upcoming.name
milestone_title = Milestone::Upcoming.title
end
h(milestone_title.presence || default_label)
end
private private
def sidebar_gutter_collapsed? def sidebar_gutter_collapsed?
......
...@@ -5,8 +5,10 @@ module NotesHelper ...@@ -5,8 +5,10 @@ module NotesHelper
end end
def note_target_fields(note) def note_target_fields(note)
hidden_field_tag(:target_type, note.noteable.class.name.underscore) + if note.noteable
hidden_field_tag(:target_id, note.noteable.id) hidden_field_tag(:target_type, note.noteable.class.name.underscore) +
hidden_field_tag(:target_id, note.noteable.id)
end
end end
def note_editable?(note) def note_editable?(note)
......
...@@ -110,6 +110,10 @@ class Notify < BaseMailer ...@@ -110,6 +110,10 @@ class Notify < BaseMailer
headers['Reply-To'] = address headers['Reply-To'] = address
fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze
headers['References'] ||= ''
headers['References'] << ' ' << fallback_reply_message_id
@reply_by_email = true @reply_by_email = true
end end
......
...@@ -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,24 +348,22 @@ class Ability ...@@ -338,24 +348,22 @@ 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
rules += [
:"read_#{name}",
:"update_#{name}",
:"admin_#{name}"
]
end
if subject.respond_to?(:project) && subject.project if note.author == user
rules += project_abilities(user, subject.project) rules += [
end :read_note,
:update_note,
:admin_note
]
end
rules if note.respond_to?(:project) && note.project
rules += project_abilities(user, note.project)
end end
rules
end end
def personal_snippet_abilities(user, snippet) def personal_snippet_abilities(user, snippet)
...@@ -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
......
...@@ -14,6 +14,7 @@ class GlobalMilestone ...@@ -14,6 +14,7 @@ class GlobalMilestone
def initialize(title, milestones) def initialize(title, milestones)
@title = title @title = title
@name = title
@milestones = milestones @milestones = milestones
end end
......
...@@ -279,7 +279,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -279,7 +279,7 @@ class MergeRequest < ActiveRecord::Base
WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
def work_in_progress? def work_in_progress?
title =~ WIP_REGEX !!(title =~ WIP_REGEX)
end end
def wipless_title def wipless_title
...@@ -331,15 +331,15 @@ class MergeRequest < ActiveRecord::Base ...@@ -331,15 +331,15 @@ class MergeRequest < ActiveRecord::Base
# Returns the raw diff for this merge request # Returns the raw diff for this merge request
# #
# see "git diff" # see "git diff"
def to_diff(current_user) def to_diff
target_project.repository.diff_text(target_branch, source_sha) target_project.repository.diff_text(diff_base_commit.sha, source_sha)
end end
# Returns the commit as a series of email patches. # Returns the commit as a series of email patches.
# #
# see "git format-patch" # see "git format-patch"
def to_patch(current_user) def to_patch
target_project.repository.format_patch(target_branch, source_sha) target_project.repository.format_patch(diff_base_commit.sha, source_sha)
end end
def hook_attrs def hook_attrs
......
...@@ -22,7 +22,7 @@ class SlackService ...@@ -22,7 +22,7 @@ class SlackService
@issue_url = obj_attr[:url] @issue_url = obj_attr[:url]
@action = obj_attr[:action] @action = obj_attr[:action]
@state = obj_attr[:state] @state = obj_attr[:state]
@description = obj_attr[:description] @description = obj_attr[:description] || ''
end end
def attachments def attachments
......
.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
%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo) } %li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} }
.todo-item.todo-block .todo-item.todo-block
= image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:'' = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
......
...@@ -56,19 +56,20 @@ ...@@ -56,19 +56,20 @@
.prepend-top-default .prepend-top-default
= f.submit 'Update settings', class: "btn btn-create" = f.submit 'Update settings', class: "btn btn-create"
%hr %hr
%h5 .col-lg-9.col-lg-push-3
Groups (#{@group_members.count}) %h5
%div Groups (#{@group_members.count})
%ul.bordered-list %div
- @group_members.each do |group_member| %ul.bordered-list
- notification = Notification.new(group_member) - @group_members.each do |group_member|
= render 'settings', type: 'group', membership: group_member, notification: notification - notification = Notification.new(group_member)
%h5 = render 'settings', type: 'group', membership: group_member, notification: notification
Projects (#{@project_members.count}) %h5
%p.account-well Projects (#{@project_members.count})
To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group. %p.account-well
.append-bottom-default To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group.
%ul.bordered-list .append-bottom-default
- @project_members.each do |project_member| %ul.bordered-list
- notification = Notification.new(project_member) - @project_members.each do |project_member|
= render 'settings', type: 'project', membership: project_member, notification: notification - notification = Notification.new(project_member)
= render 'settings', type: 'project', membership: project_member, notification: notification
...@@ -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
...@@ -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?
......
- note = discussion_notes.first - note = discussion_notes.first
- commit = note.noteable
- commit_description = commit ? 'commit' : 'a deleted commit'
.discussion.js-toggle-container{ class: note.discussion_id } .discussion.js-toggle-container{ class: note.discussion_id }
.discussion-header .discussion-header
.discussion-actions .discussion-actions
...@@ -7,8 +9,9 @@ ...@@ -7,8 +9,9 @@
Show/hide discussion Show/hide discussion
%div %div
= link_to_member(@project, note.author, avatar: false) = link_to_member(@project, note.author, avatar: false)
started a discussion on commit %p started a discussion on #{commit_description}
= link_to(note.noteable.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace') - if commit
= link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace')
.last-update.hide.js-toggle-content .last-update.hide.js-toggle-content
- last_note = discussion_notes.last - last_note = discussion_notes.last
last updated by last updated by
......
...@@ -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
......
- 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(milestone_dropdown_label(params[:milestone_title]), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', 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, show_upcoming: 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
- if can? current_user, :admin_milestone, @project - if can? current_user, :admin_milestone, @project
......
...@@ -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,8 @@ ...@@ -116,5 +146,8 @@
= 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')
new Sidebar();
\ No newline at end of file
...@@ -106,7 +106,7 @@ production: &base ...@@ -106,7 +106,7 @@ production: &base
enabled: false enabled: false
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
# The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "gitlab-incoming+%{key}@gmail.com" address: "gitlab-incoming+%{key}@gmail.com"
# Email account username # Email account username
......
...@@ -17,7 +17,7 @@ if File.exists?(config_file) ...@@ -17,7 +17,7 @@ if File.exists?(config_file)
config['start_tls'] = false if config['start_tls'].nil? config['start_tls'] = false if config['start_tls'].nil?
config['mailbox'] = "inbox" if config['mailbox'].nil? config['mailbox'] = "inbox" if config['mailbox'].nil?
if config['enabled'] && config['address'] && config['address'].include?('%{key}') if config['enabled'] && config['address']
redis_url = Gitlab::RedisConfig.new(rails_env).url redis_url = Gitlab::RedisConfig.new(rails_env).url
%> %>
- -
......
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
...@@ -8,9 +8,9 @@ Get all labels for a given project. ...@@ -8,9 +8,9 @@ Get all labels for a given project.
GET /projects/:id/labels GET /projects/:id/labels
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ------- | -------- | --------------------- |
| `id` | integer | yes | The ID of the project | | `id` | integer | yes | The ID of the project |
```bash ```bash
curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/labels curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/labels
...@@ -22,35 +22,43 @@ Example response: ...@@ -22,35 +22,43 @@ Example response:
[ [
{ {
"name" : "bug", "name" : "bug",
"color" : "#d9534f" "color" : "#d9534f",
"description": "Bug reported by user"
}, },
{ {
"color" : "#d9534f", "color" : "#d9534f",
"name" : "confirmed" "name" : "confirmed",
"description": "Confirmed issue"
}, },
{ {
"name" : "critical", "name" : "critical",
"color" : "#d9534f" "color" : "#d9534f",
"description": "Criticalissue. Need fix ASAP"
}, },
{ {
"color" : "#428bca", "color" : "#428bca",
"name" : "discussion" "name" : "discussion",
"description": "Issue that needs further discussion"
}, },
{ {
"name" : "documentation", "name" : "documentation",
"color" : "#f0ad4e" "color" : "#f0ad4e",
"description": "Issue about documentation"
}, },
{ {
"color" : "#5cb85c", "color" : "#5cb85c",
"name" : "enhancement" "name" : "enhancement",
"description": "Enhancement proposal"
}, },
{ {
"color" : "#428bca", "color" : "#428bca",
"name" : "suggestion" "name" : "suggestion",
"description": "Suggestion"
}, },
{ {
"color" : "#f0ad4e", "color" : "#f0ad4e",
"name" : "support" "name" : "support",
"description": "Support issue"
} }
] ]
``` ```
...@@ -66,11 +74,12 @@ and 409 if the label already exists. ...@@ -66,11 +74,12 @@ and 409 if the label already exists.
POST /projects/:id/labels POST /projects/:id/labels
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | ------------- | ------- | -------- | ---------------------------- |
| `id` | integer | yes | The ID of the project | | `id` | integer | yes | The ID of the project |
| `name` | string | yes | The name of the label | | `name` | string | yes | The name of the label |
| `color` | string | yes | The color of the label in 6-digit hex notation with leading `#` sign | | `color` | string | yes | The color of the label in 6-digit hex notation with leading `#` sign |
| `description` | string | no | The description of the label |
```bash ```bash
curl --data "name=feature&color=#5843AD" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels" curl --data "name=feature&color=#5843AD" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels"
...@@ -81,7 +90,8 @@ Example response: ...@@ -81,7 +90,8 @@ Example response:
```json ```json
{ {
"name" : "feature", "name" : "feature",
"color" : "#5843AD" "color" : "#5843AD",
"description":null
} }
``` ```
...@@ -97,10 +107,10 @@ In case of an error, an additional error message is returned. ...@@ -97,10 +107,10 @@ In case of an error, an additional error message is returned.
DELETE /projects/:id/labels DELETE /projects/:id/labels
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ------- | -------- | --------------------- |
| `id` | integer | yes | The ID of the project | | `id` | integer | yes | The ID of the project |
| `name` | string | yes | The name of the label | | `name` | string | yes | The name of the label |
```bash ```bash
curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug" curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug"
...@@ -112,6 +122,7 @@ Example response: ...@@ -112,6 +122,7 @@ Example response:
{ {
"title" : "feature", "title" : "feature",
"color" : "#5843AD", "color" : "#5843AD",
"description": "New feature proposal",
"updated_at" : "2015-11-03T21:22:30.737Z", "updated_at" : "2015-11-03T21:22:30.737Z",
"template" : false, "template" : false,
"project_id" : 1, "project_id" : 1,
...@@ -133,15 +144,16 @@ In case of an error, an additional error message is returned. ...@@ -133,15 +144,16 @@ In case of an error, an additional error message is returned.
PUT /projects/:id/labels PUT /projects/:id/labels
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------------- | ------- | --------------------------------- | ------------------------------- |
| `id` | integer | yes | The ID of the project | | `id` | integer | yes | The ID of the project |
| `name` | string | yes | The name of the existing label | | `name` | string | yes | The name of the existing label |
| `new_name` | string | yes if `color` if not provided | The new name of the label | | `new_name` | string | yes if `color` if not provided | The new name of the label |
| `color` | string | yes if `new_name` is not provided | The new color of the label in 6-digit hex notation with leading `#` sign | | `color` | string | yes if `new_name` is not provided | The new color of the label in 6-digit hex notation with leading `#` sign |
| `description` | string | no | The new description of the label |
```bash ```bash
curl -X PUT --data "name=documentation&new_name=docs&color=#8E44AD" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels" curl -X PUT --data "name=documentation&new_name=docs&color=#8E44AD&description=Documentation" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels"
``` ```
Example response: Example response:
...@@ -149,6 +161,7 @@ Example response: ...@@ -149,6 +161,7 @@ Example response:
```json ```json
{ {
"color" : "#8E44AD", "color" : "#8E44AD",
"name" : "docs" "name" : "docs",
"description": "Documentation"
} }
``` ```
# Reply by email # Reply by email
GitLab can be set up to allow users to comment on issues and merge requests by replying to notification emails. GitLab can be set up to allow users to comment on issues and merge requests by
replying to notification emails.
## Get a mailbox ## Requirement
Reply by email requires an IMAP-enabled email account, with a provider or server that supports [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing). Sub-addressing is a feature where any email to `user+some_arbitrary_tag@example.com` will end up in the mailbox for `user@example.com`, and is supported by providers such as Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix mail server which you can run on-premises. Reply by email requires an IMAP-enabled email account. GitLab allows you to use
three strategies for this feature:
- using email sub-addressing
- using a dedicated email address
- using a catch-all mailbox
If you want to use Gmail / Google Apps with Reply by email, make sure you have [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) and [allow less secure apps to access the account](https://support.google.com/accounts/answer/6010255). ### Email sub-addressing
To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these instructions](./postfix.md). **If your provider or server supports email sub-addressing, we recommend using it.**
[Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is
a feature where any email to `user+some_arbitrary_tag@example.com` will end up
in the mailbox for `user@example.com`, and is supported by providers such as
Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix
mail server which you can run on-premises.
### Dedicated email address
This solution is really simple to set up: you just have to create an email
address dedicated to receive your users' replies to GitLab notifications.
### Catch-all mailbox
A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain will
"catch all" the emails addressed to the domain that do not exist in the mail
server.
## How it works?
### 1. GitLab sends a notification email
When GitLab sends a notification and Reply by email is enabled, the `Reply-To`
header is set to the address defined in your GitLab configuration, with the
`%{key}` placeholder (if present) replaced by a specific "reply key". In
addition, this "reply key" is also added to the `References` header.
### 2. You reply to the notification email
When you reply to the notification email, your email client will:
- send the email to the `Reply-To` address it got from the notification email
- set the `In-Reply-To` header to the value of the `Message-ID` header from the
notification email
- set the `References` header to the value of the `Message-ID` plus the value of
the notification email's `References` header.
### 3. GitLab receives your reply to the notification email
When GitLab receives your reply, it will look for the "reply key" in the
following headers, in this order:
1. the `To` header
1. the `References` header
If it finds a reply key, it will be able to leave your reply as a comment on
the entity the notification was about (issue, merge request, commit...).
For more details about the `Message-ID`, `In-Reply-To`, and `References headers`,
please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4).
## Set it up ## Set it up
If you want to use Gmail / Google Apps with Reply by email, make sure you have
[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018)
and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
[these instructions](./postfix.md).
### Omnibus package installations ### Omnibus package installations
1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the feature and fill in the details for your specific IMAP server and email account: 1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the
feature and fill in the details for your specific IMAP server and email account:
```ruby ```ruby
# Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
gitlab_rails['incoming_email_enabled'] = true gitlab_rails['incoming_email_enabled'] = true
# The email address including a placeholder for the key that references the item being replied to. # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
# The `%{key}` placeholder is added after the user part, before the `@`. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com" gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com"
# Email account username # Email account username
# With third party providers, this is usually the full email address. # With third party providers, this is usually the full email address.
# With self-hosted email servers, this is usually the user part of the email address. # With self-hosted email servers, this is usually the user part of the email address.
gitlab_rails['incoming_email_email'] = "incoming" gitlab_rails['incoming_email_email'] = "incoming"
# Email account password # Email account password
gitlab_rails['incoming_email_password'] = "[REDACTED]" gitlab_rails['incoming_email_password'] = "[REDACTED]"
# IMAP server host # IMAP server host
gitlab_rails['incoming_email_host'] = "gitlab.example.com" gitlab_rails['incoming_email_host'] = "gitlab.example.com"
# IMAP server port # IMAP server port
...@@ -47,18 +110,18 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -47,18 +110,18 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
```ruby ```ruby
# Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
gitlab_rails['incoming_email_enabled'] = true gitlab_rails['incoming_email_enabled'] = true
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
# The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com" gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com"
# Email account username # Email account username
# With third party providers, this is usually the full email address. # With third party providers, this is usually the full email address.
# With self-hosted email servers, this is usually the user part of the email address. # With self-hosted email servers, this is usually the user part of the email address.
gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com" gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com"
# Email account password # Email account password
gitlab_rails['incoming_email_password'] = "[REDACTED]" gitlab_rails['incoming_email_password'] = "[REDACTED]"
# IMAP server host # IMAP server host
gitlab_rails['incoming_email_host'] = "imap.gmail.com" gitlab_rails['incoming_email_host'] = "imap.gmail.com"
# IMAP server port # IMAP server port
...@@ -72,8 +135,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -72,8 +135,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
gitlab_rails['incoming_email_mailbox_name'] = "inbox" gitlab_rails['incoming_email_mailbox_name'] = "inbox"
``` ```
As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`.
1. Reconfigure GitLab and restart mailroom for the changes to take effect: 1. Reconfigure GitLab and restart mailroom for the changes to take effect:
```sh ```sh
...@@ -97,7 +158,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -97,7 +158,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
cd /home/git/gitlab cd /home/git/gitlab
``` ```
1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account: 1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature
and fill in the details for your specific IMAP server and email account:
```sh ```sh
sudo editor config/gitlab.yml sudo editor config/gitlab.yml
...@@ -109,7 +171,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -109,7 +171,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
enabled: true enabled: true
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
# The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "incoming+%{key}@gitlab.example.com" address: "incoming+%{key}@gitlab.example.com"
# Email account username # Email account username
...@@ -138,7 +200,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -138,7 +200,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
enabled: true enabled: true
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
# The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "gitlab-incoming+%{key}@gmail.com" address: "gitlab-incoming+%{key}@gmail.com"
# Email account username # Email account username
...@@ -161,8 +223,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -161,8 +223,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
mailbox: "inbox" mailbox: "inbox"
``` ```
As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`.
1. Enable `mail_room` in the init script at `/etc/default/gitlab`: 1. Enable `mail_room` in the init script at `/etc/default/gitlab`:
```sh ```sh
...@@ -195,8 +255,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -195,8 +255,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
incoming_email: incoming_email:
enabled: true enabled: true
# The email address including a placeholder for the key that references the item being replied to. # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
# The `%{key}` placeholder is added after the user part, before the `@`. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "gitlab-incoming+%{key}@gmail.com" address: "gitlab-incoming+%{key}@gmail.com"
# Email account username # Email account username
......
...@@ -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).
......
...@@ -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
......
...@@ -36,3 +36,8 @@ Feature: Dashboard Todos ...@@ -36,3 +36,8 @@ Feature: Dashboard Todos
Scenario: I filter by action Scenario: I filter by action
Given I filter by "Mentioned" Given I filter by "Mentioned"
Then I should not see todos related to "Assignments" in the list Then I should not see todos related to "Assignments" in the list
@javascript
Scenario: I click on a todo row
Given I click on the todo
Then I should be directed to the corresponding page
...@@ -88,6 +88,14 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps ...@@ -88,6 +88,14 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
should_not_see_todo "John Doe assigned you issue ##{issue.iid}" should_not_see_todo "John Doe assigned you issue ##{issue.iid}"
end end
step 'I click on the todo' do
find('.todo:nth-child(1)').click
end
step 'I should be directed to the corresponding page' do
page.should have_css('.identifier', text: 'Merge Request !1')
end
def should_see_todo(position, title, body, pending = true) def should_see_todo(position, title, body, pending = true)
page.within(".todo:nth-child(#{position})") do page.within(".todo:nth-child(#{position})") do
expect(page).to have_content title expect(page).to have_content title
......
This diff is collapsed.
...@@ -292,7 +292,7 @@ module API ...@@ -292,7 +292,7 @@ module API
end end
class Label < Grape::Entity class Label < Grape::Entity
expose :name, :color expose :name, :color, :description
end end
class Compare < Grape::Entity class Compare < Grape::Entity
......
...@@ -17,17 +17,18 @@ module API ...@@ -17,17 +17,18 @@ module API
# Creates a new label # Creates a new label
# #
# Parameters: # Parameters:
# id (required) - The ID of a project # id (required) - The ID of a project
# name (required) - The name of the label to be deleted # name (required) - The name of the label to be created
# color (required) - Color of the label given in 6-digit hex # color (required) - Color of the label given in 6-digit hex
# notation with leading '#' sign (e.g. #FFAABB) # notation with leading '#' sign (e.g. #FFAABB)
# description (optional) - The description of label to be created
# Example Request: # Example Request:
# POST /projects/:id/labels # POST /projects/:id/labels
post ':id/labels' do post ':id/labels' do
authorize! :admin_label, user_project authorize! :admin_label, user_project
required_attributes! [:name, :color] required_attributes! [:name, :color]
attrs = attributes_for_keys [:name, :color] attrs = attributes_for_keys [:name, :color, :description]
label = user_project.find_label(attrs[:name]) label = user_project.find_label(attrs[:name])
conflict!('Label already exists') if label conflict!('Label already exists') if label
...@@ -62,11 +63,12 @@ module API ...@@ -62,11 +63,12 @@ module API
# Updates an existing label. At least one optional parameter is required. # Updates an existing label. At least one optional parameter is required.
# #
# Parameters: # Parameters:
# id (required) - The ID of a project # id (required) - The ID of a project
# name (required) - The name of the label to be deleted # name (required) - The name of the label to be deleted
# new_name (optional) - The new name of the label # new_name (optional) - The new name of the label
# color (optional) - Color of the label given in 6-digit hex # color (optional) - Color of the label given in 6-digit hex
# notation with leading '#' sign (e.g. #FFAABB) # notation with leading '#' sign (e.g. #FFAABB)
# description (optional) - The description of label to be created
# Example Request: # Example Request:
# PUT /projects/:id/labels # PUT /projects/:id/labels
put ':id/labels' do put ':id/labels' do
...@@ -76,7 +78,7 @@ module API ...@@ -76,7 +78,7 @@ module API
label = user_project.find_label(params[:name]) label = user_project.find_label(params[:name])
not_found!('Label not found') unless label not_found!('Label not found') unless label
attrs = attributes_for_keys [:new_name, :color] attrs = attributes_for_keys [:new_name, :color, :description]
if attrs.empty? if attrs.empty?
render_api_error!('Required parameters "new_name" or "color" ' \ render_api_error!('Required parameters "new_name" or "color" ' \
......
...@@ -48,4 +48,23 @@ class AwardEmoji ...@@ -48,4 +48,23 @@ class AwardEmoji
JSON.parse(File.read(json_path)) JSON.parse(File.read(json_path))
end end
end end
# Returns an Array of Emoji names and their asset URLs.
def self.urls
@urls ||= begin
path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
prefix = Gitlab::Application.config.assets.prefix
digest = Gitlab::Application.config.assets.digest
JSON.parse(File.read(path)).map do |hash|
if digest
fname = "#{hash['unicode']}-#{hash['digest']}"
else
fname = hash['unicode']
end
{ name: hash['name'], path: "#{prefix}/#{fname}.png" }
end
end
end
end end
require 'active_support/core_ext/string/output_safety'
module Banzai module Banzai
module Filter module Filter
def self.[](name) def self.[](name)
......
require 'html/pipeline/filter'
require 'uri' require 'uri'
module Banzai module Banzai
......
require 'action_controller'
require 'gitlab_emoji'
require 'html/pipeline/filter'
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces :emoji: with images. # HTML filter that replaces :emoji: with images.
......
require 'html/pipeline/filter'
module Banzai module Banzai
module Filter module Filter
# HTML Filter to add a `rel="nofollow"` attribute to external links # HTML Filter to add a `rel="nofollow"` attribute to external links
......
require 'banzai'
require 'html/pipeline/filter'
module Banzai module Banzai
module Filter module Filter
# HTML Filter for parsing Gollum's tags in HTML. It's only parses the # HTML Filter for parsing Gollum's tags in HTML. It's only parses the
......
require 'html/pipeline/filter'
module Banzai module Banzai
module Filter module Filter
class MarkdownFilter < HTML::Pipeline::TextFilter class MarkdownFilter < HTML::Pipeline::TextFilter
......
require 'banzai'
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces milestone references with links. # HTML filter that replaces milestone references with links.
......
require 'html/pipeline/filter'
module Banzai module Banzai
module Filter module Filter
# HTML filter that removes references to records that the current user does # HTML filter that removes references to records that the current user does
......
require 'active_support/core_ext/string/output_safety'
require 'html/pipeline/filter'
module Banzai module Banzai
module Filter module Filter
# Base class for GitLab Flavored Markdown reference filters. # Base class for GitLab Flavored Markdown reference filters.
......
require 'html/pipeline/filter'
module Banzai module Banzai
module Filter module Filter
# HTML filter that gathers all referenced records that the current user has # HTML filter that gathers all referenced records that the current user has
......
require 'html/pipeline/filter'
require 'uri' require 'uri'
module Banzai module Banzai
......
require 'html/pipeline/filter'
require 'html/pipeline/sanitization_filter'
module Banzai module Banzai
module Filter module Filter
# Sanitize HTML # Sanitize HTML
......
require 'html/pipeline/filter'
require 'rouge/plugins/redcarpet' require 'rouge/plugins/redcarpet'
module Banzai module Banzai
......
require 'html/pipeline/filter'
module Banzai module Banzai
module Filter module Filter
# HTML filter that adds an anchor child element to all Headers in a # HTML filter that adds an anchor child element to all Headers in a
......
require 'html/pipeline/filter'
require 'uri' require 'uri'
module Banzai module Banzai
......
require 'html/pipeline/filter'
require 'yaml'
module Banzai module Banzai
module Filter module Filter
class YamlFrontMatterFilter < HTML::Pipeline::Filter class YamlFrontMatterFilter < HTML::Pipeline::Filter
......
require 'html/pipeline'
module Banzai module Banzai
module Pipeline module Pipeline
class BasePipeline class BasePipeline
......
require 'banzai'
module Banzai module Banzai
module Pipeline module Pipeline
class WikiPipeline < FullPipeline class WikiPipeline < FullPipeline
......
module Gitlab
module Badge
##
# Build badge
#
class Build
def initialize(project, ref)
@image = ::Ci::ImageForBuildService.new.execute(project, ref: ref)
end
def to_s
@image[:name].sub(/\.svg$/, '')
end
def type
'image/svg+xml'
end
def data
File.read(@image[:path])
end
end
end
end
...@@ -63,6 +63,10 @@ module Gitlab ...@@ -63,6 +63,10 @@ module Gitlab
end end
def reply_key def reply_key
key_from_to_header || key_from_additional_headers
end
def key_from_to_header
key = nil key = nil
message.to.each do |address| message.to.each do |address|
key = Gitlab::IncomingEmail.key_from_address(address) key = Gitlab::IncomingEmail.key_from_address(address)
...@@ -72,6 +76,17 @@ module Gitlab ...@@ -72,6 +76,17 @@ module Gitlab
key key
end end
def key_from_additional_headers
reply_key = nil
Array(message.references).each do |message_id|
reply_key = Gitlab::IncomingEmail.key_from_fallback_reply_message_id(message_id)
break if reply_key
end
reply_key
end
def sent_notification def sent_notification
return nil unless reply_key return nil unless reply_key
......
module Gitlab module Gitlab
module IncomingEmail module IncomingEmail
class << self class << self
def enabled? FALLBACK_REPLY_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze
config.enabled && address_formatted_correctly?
end
def address_formatted_correctly? def enabled?
config.address && config.enabled && config.address
config.address.include?("%{key}")
end end
def reply_address(key) def reply_address(key)
...@@ -24,6 +21,13 @@ module Gitlab ...@@ -24,6 +21,13 @@ module Gitlab
match[1] match[1]
end end
def key_from_fallback_reply_message_id(message_id)
match = message_id.match(FALLBACK_REPLY_MESSAGE_ID_REGEX)
return unless match
match[1]
end
def config def config
Gitlab.config.incoming_email Gitlab.config.incoming_email
end end
......
# This task will generate a standard and Retina sprite of all of the current
# Gemojione Emojis, with the accompanying SCSS map.
#
# It will not appear in `rake -T` output, and the dependent gems are not
# included in the Gemfile by default, because this task will only be needed
# occasionally, such as when new Emojis are added to Gemojione.
begin
require 'sprite_factory'
require 'rmagick'
rescue LoadError
# noop
end
namespace :gemojione do namespace :gemojione do
desc 'Generates Emoji SHA256 digests'
task digests: :environment do
require 'digest/sha2'
require 'json'
dir = Gemojione.index.images_path
digests = AwardEmoji.emojis.map do |name, emoji_hash|
fpath = File.join(dir, "#{emoji_hash['unicode']}.png")
digest = Digest::SHA256.file(fpath).hexdigest
{ name: name, unicode: emoji_hash['unicode'], digest: digest }
end
out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
File.open(out, 'w') do |handle|
handle.write(JSON.pretty_generate(digests))
end
end
# This task will generate a standard and Retina sprite of all of the current
# Gemojione Emojis, with the accompanying SCSS map.
#
# It will not appear in `rake -T` output, and the dependent gems are not
# included in the Gemfile by default, because this task will only be needed
# occasionally, such as when new Emojis are added to Gemojione.
task sprite: :environment do task sprite: :environment do
begin
require 'sprite_factory'
require 'rmagick'
rescue LoadError
# noop
end
check_requirements! check_requirements!
SIZE = 20 SIZE = 20
......
...@@ -623,7 +623,6 @@ namespace :gitlab do ...@@ -623,7 +623,6 @@ namespace :gitlab do
start_checking "Reply by email" start_checking "Reply by email"
if Gitlab.config.incoming_email.enabled if Gitlab.config.incoming_email.enabled
check_address_formatted_correctly
check_imap_authentication check_imap_authentication
if Rails.env.production? if Rails.env.production?
...@@ -643,20 +642,6 @@ namespace :gitlab do ...@@ -643,20 +642,6 @@ namespace :gitlab do
# Checks # Checks
######################## ########################
def check_address_formatted_correctly
print "Address formatted correctly? ... "
if Gitlab::IncomingEmail.address_formatted_correctly?
puts "yes".green
else
puts "no".red
try_fixing_it(
"Make sure that the address in config/gitlab.yml includes the '%{key}' placeholder."
)
fix_and_rerun
end
end
def check_initd_configured_correctly def check_initd_configured_correctly
print "Init.d configured correctly? ... " print "Init.d configured correctly? ... "
......
...@@ -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
# #
......
...@@ -63,7 +63,7 @@ describe Projects::MergeRequestsController do ...@@ -63,7 +63,7 @@ describe Projects::MergeRequestsController do
id: merge_request.iid, id: merge_request.iid,
format: format) format: format)
expect(response.body).to eq((merge_request.send(:"to_#{format}",user)).to_s) expect(response.body).to eq((merge_request.send(:"to_#{format}")).to_s)
end end
it "should not escape Html" do it "should not escape Html" do
......
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
...@@ -11,7 +11,41 @@ feature 'Issue filtering by Milestone', feature: true do ...@@ -11,7 +11,41 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project) visit_issues(project)
filter_by_milestone(Milestone::None.title) filter_by_milestone(Milestone::None.title)
expect(page).to have_css('.issue .title', count: 1) expect(page).to have_css('.issue', count: 1)
end
context 'filters by upcoming milestone', js: true do
it 'should not show issues with no expiry' do
create(:issue, project: project)
create(:issue, project: project, milestone: milestone)
visit_issues(project)
filter_by_milestone(Milestone::Upcoming.title)
expect(page).to have_css('.issue', count: 0)
end
it 'should show issues in future' do
milestone = create(:milestone, project: project, due_date: Date.tomorrow)
create(:issue, project: project)
create(:issue, project: project, milestone: milestone)
visit_issues(project)
filter_by_milestone(Milestone::Upcoming.title)
expect(page).to have_css('.issue', count: 1)
end
it 'should not show issues in past' do
milestone = create(:milestone, project: project, due_date: Date.yesterday)
create(:issue, project: project)
create(:issue, project: project, milestone: milestone)
visit_issues(project)
filter_by_milestone(Milestone::Upcoming.title)
expect(page).to have_css('.issue', count: 0)
end
end end
scenario 'filters by a specific Milestone', js: true do scenario 'filters by a specific Milestone', js: true do
...@@ -21,7 +55,7 @@ feature 'Issue filtering by Milestone', feature: true do ...@@ -21,7 +55,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project) visit_issues(project)
filter_by_milestone(milestone.title) filter_by_milestone(milestone.title)
expect(page).to have_css('.issue .title', count: 1) expect(page).to have_css('.issue', count: 1)
end end
def visit_issues(project) def visit_issues(project)
...@@ -30,8 +64,6 @@ feature 'Issue filtering by Milestone', feature: true do ...@@ -30,8 +64,6 @@ 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 find(".milestone-filter .dropdown-content a", text: title).click
find(".milestone-filter a", text: title).click
sleep 1
end end
end end
...@@ -34,20 +34,7 @@ describe 'Issues', feature: true do ...@@ -34,20 +34,7 @@ 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
...@@ -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
......
...@@ -11,7 +11,41 @@ feature 'Merge Request filtering by Milestone', feature: true do ...@@ -11,7 +11,41 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests(project) visit_merge_requests(project)
filter_by_milestone(Milestone::None.title) filter_by_milestone(Milestone::None.title)
expect(page).to have_css('.merge-request-title', count: 1) expect(page).to have_css('.merge-request', count: 1)
end
context 'filters by upcoming milestone', js: true do
it 'should not show issues with no expiry' do
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
visit_merge_requests(project)
filter_by_milestone(Milestone::Upcoming.title)
expect(page).to have_css('.merge-request', count: 0)
end
it 'should show issues in future' do
milestone = create(:milestone, project: project, due_date: Date.tomorrow)
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
visit_merge_requests(project)
filter_by_milestone(Milestone::Upcoming.title)
expect(page).to have_css('.merge-request', count: 1)
end
it 'should not show issues in past' do
milestone = create(:milestone, project: project, due_date: Date.yesterday)
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
visit_merge_requests(project)
filter_by_milestone(Milestone::Upcoming.title)
expect(page).to have_css('.merge-request', count: 0)
end
end end
scenario 'filters by a specific Milestone', js: true do scenario 'filters by a specific Milestone', js: true do
...@@ -21,7 +55,7 @@ feature 'Merge Request filtering by Milestone', feature: true do ...@@ -21,7 +55,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests(project) visit_merge_requests(project)
filter_by_milestone(milestone.title) filter_by_milestone(milestone.title)
expect(page).to have_css('.merge-request-title', count: 1) expect(page).to have_css('.merge-request', count: 1)
end end
def visit_merge_requests(project) def visit_merge_requests(project)
......
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
Return-Path: <jake@adventuretime.ooo>
Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
Date: Thu, 13 Jun 2013 17:03:48 -0400
From: Jake the Dog <jake@adventuretime.ooo>
To: reply@appmail.adventuretime.ooo
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
In-Reply-To: <issue_1@localhost>
References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
Mime-Version: 1.0
Content-Type: text/plain;
charset=ISO-8859-1
Content-Transfer-Encoding: 7bit
X-Sieve: CMU Sieve 2.2
X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
13 Jun 2013 14:03:48 -0700 (PDT)
X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
I could not disagree more. I am obviously biased but adventure time is the
greatest show ever created. Everyone should watch it.
- Jake out
On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
>
>
>
> eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
>
> ---
> hey guys everyone knows adventure time sucks!
>
> ---
> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
>
> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
>
...@@ -7,6 +7,8 @@ Date: Thu, 13 Jun 2013 17:03:48 -0400 ...@@ -7,6 +7,8 @@ Date: Thu, 13 Jun 2013 17:03:48 -0400
From: Jake the Dog <jake@adventuretime.ooo> From: Jake the Dog <jake@adventuretime.ooo>
To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
In-Reply-To: <issue_1@localhost>
References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux' Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
Mime-Version: 1.0 Mime-Version: 1.0
Content-Type: text/plain; Content-Type: text/plain;
...@@ -37,4 +39,4 @@ On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta ...@@ -37,4 +39,4 @@ On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3 > Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
> >
> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences). > To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
> >
\ No newline at end of file
require 'spec_helper'
describe AwardEmoji do
describe '.urls' do
subject { AwardEmoji.urls }
it { is_expected.to be_an_instance_of(Array) }
it { is_expected.to_not be_empty }
context 'every Hash in the Array' do
it 'has the correct keys and values' do
subject.each do |hash|
expect(hash[:name]).to be_an_instance_of(String)
expect(hash[:path]).to be_an_instance_of(String)
end
end
end
end
end
require 'spec_helper'
describe Gitlab::Badge::Build do
let(:project) { create(:project) }
let(:sha) { project.commit.sha }
let(:badge) { described_class.new(project, 'master') }
describe '#type' do
subject { badge.type }
it { is_expected.to eq 'image/svg+xml' }
end
context 'build exists' do
let(:ci_commit) { create(:ci_commit, project: project, sha: sha) }
let!(:build) { create(:ci_build, commit: ci_commit) }
context 'build success' do
before { build.success! }
describe '#to_s' do
subject { badge.to_s }
it { is_expected.to eq 'build-success' }
end
describe '#data' do
let(:data) { badge.data }
it 'contains infromation about success' do
expect(status_node(data, 'success')).to be_truthy
end
end
end
context 'build failed' do
before { build.drop! }
describe '#to_s' do
subject { badge.to_s }
it { is_expected.to eq 'build-failed' }
end
describe '#data' do
let(:data) { badge.data }
it 'contains infromation about failure' do
expect(status_node(data, 'failed')).to be_truthy
end
end
end
end
context 'build does not exist' do
describe '#to_s' do
subject { badge.to_s }
it { is_expected.to eq 'build-unknown' }
end
describe '#data' do
let(:data) { badge.data }
it 'contains infromation about unknown build' do
expect(status_node(data, 'unknown')).to be_truthy
end
end
end
def status_node(data, status)
xml = Nokogiri::XML.parse(data)
xml.at(%Q{text:contains("#{status}")})
end
end
...@@ -3,6 +3,7 @@ require "spec_helper" ...@@ -3,6 +3,7 @@ require "spec_helper"
describe Gitlab::Email::Receiver, lib: true do describe Gitlab::Email::Receiver, lib: true do
before do before do
stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo") stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo")
stub_config_setting(host: 'localhost')
end end
let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" } let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" }
...@@ -137,5 +138,27 @@ describe Gitlab::Email::Receiver, lib: true do ...@@ -137,5 +138,27 @@ describe Gitlab::Email::Receiver, lib: true do
expect(note.note).to include(markdown) expect(note.note).to include(markdown)
end end
context 'when sub-addressing is not supported' do
before do
stub_incoming_email_setting(enabled: true, address: nil)
end
shared_examples 'an email that contains a reply key' do |header|
it "fetches the reply key from the #{header} header and creates a comment" do
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
note = noteable.notes.last
expect(note.author).to eq(sent_notification.recipient)
expect(note.note).to include('I could not disagree more.')
end
end
context 'reply key is in the References header' do
let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references.eml') }
it_behaves_like 'an email that contains a reply key', 'References'
end
end
end end
end end
...@@ -7,24 +7,8 @@ describe Gitlab::IncomingEmail, lib: true do ...@@ -7,24 +7,8 @@ describe Gitlab::IncomingEmail, lib: true do
stub_incoming_email_setting(enabled: true) stub_incoming_email_setting(enabled: true)
end end
context "when the address is valid" do it 'returns true' do
before do expect(described_class.enabled?).to be_truthy
stub_incoming_email_setting(address: "replies+%{key}@example.com")
end
it "returns true" do
expect(described_class.enabled?).to be_truthy
end
end
context "when the address is invalid" do
before do
stub_incoming_email_setting(address: "replies@example.com")
end
it "returns false" do
expect(described_class.enabled?).to be_falsey
end
end end
end end
...@@ -58,4 +42,10 @@ describe Gitlab::IncomingEmail, lib: true do ...@@ -58,4 +42,10 @@ describe Gitlab::IncomingEmail, lib: true do
expect(described_class.key_from_address("replies+key@example.com")).to eq("key") expect(described_class.key_from_address("replies+key@example.com")).to eq("key")
end end
end end
context 'self.key_from_fallback_reply_message_id' do
it 'returns reply key' do
expect(described_class.key_from_fallback_reply_message_id('reply-key@localhost')).to eq('key')
end
end
end end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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