Commit 7ad7e10f authored by Kamil Trzcinski's avatar Kamil Trzcinski

Merge remote-tracking branch 'origin/master' into rename-ci-commit

parents 9614c522 fc809d68
...@@ -194,7 +194,7 @@ Style/EmptyLines: ...@@ -194,7 +194,7 @@ Style/EmptyLines:
# Keep blank lines around access modifiers. # Keep blank lines around access modifiers.
Style/EmptyLinesAroundAccessModifier: Style/EmptyLinesAroundAccessModifier:
Enabled: false Enabled: true
# Keeps track of empty lines around block bodies. # Keeps track of empty lines around block bodies.
Style/EmptyLinesAroundBlockBody: Style/EmptyLinesAroundBlockBody:
...@@ -771,7 +771,7 @@ Metrics/PerceivedComplexity: ...@@ -771,7 +771,7 @@ Metrics/PerceivedComplexity:
# Checks for ambiguous operators in the first argument of a method invocation # Checks for ambiguous operators in the first argument of a method invocation
# without parentheses. # without parentheses.
Lint/AmbiguousOperator: Lint/AmbiguousOperator:
Enabled: false Enabled: true
# Checks for ambiguous regexp literals in the first argument of a method # Checks for ambiguous regexp literals in the first argument of a method
# invocation without parentheses. # invocation without parentheses.
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.9.0 (unreleased) v 8.9.0 (unreleased)
- Bulk assign/unassign labels to issues.
- Allow enabling wiki page events from Webhook management UI - Allow enabling wiki page events from Webhook management UI
- Make EmailsOnPushWorker use Sidekiq mailers queue - Make EmailsOnPushWorker use Sidekiq mailers queue
- Fix wiki page events' webhook to point to the wiki repository - Fix wiki page events' webhook to point to the wiki repository
...@@ -35,13 +36,13 @@ v 8.9.0 (unreleased) ...@@ -35,13 +36,13 @@ v 8.9.0 (unreleased)
- Reduce number of queries needed to render issue labels in the sidebar - Reduce number of queries needed to render issue labels in the sidebar
- Improve error handling importing projects - Improve error handling importing projects
- Put project Files and Commits tabs under Code tab - Put project Files and Commits tabs under Code tab
- Replace Colorize with Rainbow for coloring console output in Rake tasks.
v 8.8.4
- Fix todos page throwing errors when you have a project pending deletion
- Reduce number of SQL queries when rendering user references
v 8.8.4 (unreleased) v 8.8.4 (unreleased)
- Ensure branch cleanup regardless of whether the GitHub import process succeeds - Ensure branch cleanup regardless of whether the GitHub import process succeeds
- Fix issue with arrow keys not working in search autocomplete dropdown
- Fix todos page throwing errors when you have a project pending deletion
- Reduce number of SQL queries when rendering user references
v 8.8.3 v 8.8.3
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312
......
...@@ -96,7 +96,7 @@ The designs are made using Antetype (`.atype` files). You can use the ...@@ -96,7 +96,7 @@ The designs are made using Antetype (`.atype` files). You can use the
[free Antetype viewer (Mac OSX only)] or grab an exported PNG from the design [free Antetype viewer (Mac OSX only)] or grab an exported PNG from the design
(the PNG is 1:1). (the PNG is 1:1).
The current designs can be found in the [`gitlab1.atype` file]. The current designs can be found in the [`gitlab8.atype` file].
### UI development kit ### UI development kit
...@@ -530,4 +530,4 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor ...@@ -530,4 +530,4 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide" [scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide"
[gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design [gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design
[free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12 [free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12
[`gitlab1.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/gitlab1.atype/ [`gitlab8.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/current/
...@@ -143,7 +143,7 @@ gem 'redis-namespace' ...@@ -143,7 +143,7 @@ gem 'redis-namespace'
gem "httparty", '~> 0.13.3' gem "httparty", '~> 0.13.3'
# Colored output to console # Colored output to console
gem "colorize", '~> 0.7.0' gem "rainbow", '~> 2.1.0'
# GitLab settings # GitLab settings
gem 'settingslogic', '~> 2.0.9' gem 'settingslogic', '~> 2.0.9'
......
...@@ -823,7 +823,6 @@ DEPENDENCIES ...@@ -823,7 +823,6 @@ DEPENDENCIES
carrierwave (~> 0.10.0) carrierwave (~> 0.10.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
coffee-rails (~> 4.1.0) coffee-rails (~> 4.1.0)
colorize (~> 0.7.0)
connection_pool (~> 2.0) connection_pool (~> 2.0)
coveralls (~> 0.8.2) coveralls (~> 0.8.2)
creole (~> 0.5.0) creole (~> 0.5.0)
...@@ -914,6 +913,7 @@ DEPENDENCIES ...@@ -914,6 +913,7 @@ DEPENDENCIES
rack-oauth2 (~> 1.2.1) rack-oauth2 (~> 1.2.1)
rails (= 4.2.6) rails (= 4.2.6)
rails-deprecated_sanitizer (~> 1.0.3) rails-deprecated_sanitizer (~> 1.0.3)
rainbow (~> 2.1.0)
raphael-rails (~> 2.1.2) raphael-rails (~> 2.1.2)
rblineprof rblineprof
rdoc (~> 3.6) rdoc (~> 3.6)
......
class @AwardsHandler class @AwardsHandler
constructor: (@getEmojisUrl, @postEmojiUrl, @noteableType, @noteableId, @unicodes) ->
$('.js-add-award').on 'click', (event) =>
event.stopPropagation()
event.preventDefault()
@showEmojiMenu() constructor: ->
@aliases = emojiAliases()
$(document)
.off 'click', '.js-add-award'
.on 'click', '.js-add-award', (event) =>
event.stopPropagation()
event.preventDefault()
@showEmojiMenu $(event.currentTarget)
$('html').on 'click', (event) -> $('html').on 'click', (event) ->
if !$(event.target).closest('.emoji-menu').length unless $(event.target).closest('.emoji-menu').length
if $('.emoji-menu').is(':visible') if $('.emoji-menu').is(':visible')
$('.js-add-award.is-active').removeClass 'is-active'
$('.emoji-menu').removeClass 'is-visible' $('.emoji-menu').removeClass 'is-visible'
$('.awards') $(document)
.off 'click' .off 'click', '.js-emoji-btn'
.on 'click', '.js-emoji-btn', @handleClick .on 'click', '.js-emoji-btn', @handleClick
@renderFrequentlyUsedBlock()
handleClick: (e) -> handleClick: (e) =>
e.preventDefault() e.preventDefault()
emoji = $(this)
.find('.icon')
.data 'emoji'
if emoji is 'thumbsup' and awardsHandler.didUserClickEmoji $(this), 'thumbsdown' emoji = $(e.currentTarget).find('.icon').data 'emoji'
awardsHandler.addAward 'thumbsdown' @getVotesBlock().addClass 'js-awards-block'
@addAward @getAwardUrl(), emoji
else if emoji is 'thumbsdown' and awardsHandler.didUserClickEmoji $(this), 'thumbsup'
awardsHandler.addAward 'thumbsup'
awardsHandler.addAward emoji showEmojiMenu: ($addBtn) ->
$(this).trigger 'blur' $menu = $('.emoji-menu')
didUserClickEmoji: (that, emoji) -> if $menu.length
if $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title') $holder = $addBtn.closest('.js-award-holder')
$(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title').indexOf('me') > -1
showEmojiMenu: -> if $menu.is '.is-visible'
if $('.emoji-menu').length $addBtn.removeClass 'is-active'
if $('.emoji-menu').is '.is-visible' $menu.removeClass 'is-visible'
$('.emoji-menu').removeClass 'is-visible'
$('#emoji_search').blur() $('#emoji_search').blur()
else else
$('.emoji-menu').addClass 'is-visible' $addBtn.addClass 'is-active'
@positionMenu($menu, $addBtn)
$menu.addClass 'is-visible'
$('#emoji_search').focus() $('#emoji_search').focus()
else else
$('.js-add-award').addClass 'is-loading' $addBtn.addClass 'is-loading is-active'
$.get @getEmojisUrl, (response) => url = $addBtn.data 'award-menu-url'
$('.js-add-award').removeClass 'is-loading'
$('.js-award-holder').append response @createEmojiMenu url, =>
$addBtn.removeClass 'is-loading'
$menu = $('.emoji-menu')
@positionMenu($menu, $addBtn)
@renderFrequentlyUsedBlock()
setTimeout => setTimeout =>
$('.emoji-menu').addClass 'is-visible' $menu.addClass 'is-visible'
$('#emoji_search').focus() $('#emoji_search').focus()
@setupSearch() @setupSearch()
, 200 , 200
addAward: (emoji) ->
@postEmoji emoji, => createEmojiMenu: (awardMenuUrl, callback) ->
@addAwardToEmojiBar(emoji)
$.get awardMenuUrl, (response) =>
$('body').append response
callback()
positionMenu: ($menu, $addBtn) ->
position = $addBtn.data('position')
# The menu could potentially be off-screen or in a hidden overflow element
# So we position the element absolute in the body
css =
top: "#{$addBtn.offset().top + $addBtn.outerHeight()}px"
if position? and position is 'right'
css.left = "#{($addBtn.offset().left - $menu.outerWidth()) + 20}px"
$menu.addClass 'is-aligned-right'
else
css.left = "#{$addBtn.offset().left}px"
$menu.removeClass 'is-aligned-right'
$menu.css(css)
addAward: (awardUrl, emoji, checkMutuality = yes) ->
emoji = @normilizeEmojiName(emoji)
@postEmoji awardUrl, emoji, =>
@addAwardToEmojiBar(emoji, checkMutuality)
$('.js-awards-block-current').removeClass 'js-awards-block-current'
$('.emoji-menu').removeClass 'is-visible' $('.emoji-menu').removeClass 'is-visible'
addAwardToEmojiBar: (emoji) ->
addAwardToEmojiBar: (emoji, checkForMutuality = yes) ->
@checkMutuality emoji if checkForMutuality
@addEmojiToFrequentlyUsedList(emoji) @addEmojiToFrequentlyUsedList(emoji)
if @exist(emoji) emoji = @normilizeEmojiName(emoji)
if @isActive(emoji) $emojiBtn = @findEmojiIcon(emoji).parent()
@decrementCounter(emoji)
if $emojiBtn.length > 0
if @isActive($emojiBtn)
@decrementCounter($emojiBtn, emoji)
else else
counter = @findEmojiIcon(emoji).siblings('.js-counter') counter = $emojiBtn.find('.js-counter')
counter.text(parseInt(counter.text()) + 1) counter.text(parseInt(counter.text()) + 1)
counter.parent().addClass('active') $emojiBtn.addClass('active')
@addMeToAuthorList(emoji) @addMeToUserList(emoji)
else else
@createEmoji(emoji) @createEmoji(emoji)
exist: (emoji) ->
@findEmojiIcon(emoji).length > 0 getVotesBlock: -> return $ '.awards.js-awards-block'
isActive: (emoji) ->
@findEmojiIcon(emoji).parent().hasClass('active') getAwardUrl: -> @getVotesBlock().data 'award-url'
decrementCounter: (emoji) ->
counter = @findEmojiIcon(emoji).siblings('.js-counter') checkMutuality: (emoji) ->
emojiIcon = counter.parent()
if parseInt(counter.text()) > 1 awardUrl = @getAwardUrl()
counter.text(parseInt(counter.text()) - 1)
emojiIcon.removeClass('active') if emoji in [ 'thumbsup', 'thumbsdown' ]
@removeMeFromAuthorList(emoji) mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup'
else if emoji == 'thumbsup' || emoji == 'thumbsdown'
emojiIcon.tooltip('destroy') isAlreadyVoted = $("[data-emoji=#{mutualVote}]").parent().hasClass 'active'
counter.text(0) @addAward awardUrl, mutualVote, no if isAlreadyVoted
emojiIcon.removeClass('active')
@removeMeFromAuthorList(emoji)
isActive: ($emojiBtn) -> $emojiBtn.hasClass 'active'
decrementCounter: ($emojiBtn, emoji) ->
isntNoteBody = $emojiBtn.closest('.note-body').length is 0
counter = $('.js-counter', $emojiBtn)
counterNumber = parseInt(counter.text())
if !isntNoteBody
# If this is a note body, we just hide the award emoji row like the initial state
$emojiBtn.closest('.js-awards-block').addClass 'hidden'
if counterNumber > 1
counter.text(counterNumber - 1)
@removeMeFromUserList($emojiBtn, emoji)
else if (emoji == 'thumbsup' || emoji == 'thumbsdown') && isntNoteBody
$emojiBtn.tooltip('destroy')
counter.text('0')
@removeMeFromUserList($emojiBtn, emoji)
else else
emojiIcon.tooltip('destroy') $emojiBtn.tooltip('destroy')
emojiIcon.remove() $emojiBtn.remove()
$emojiBtn.removeClass('active')
getAwardTooltip: ($awardBlock) ->
return $awardBlock.attr('data-original-title') or $awardBlock.attr('data-title')
removeMeFromUserList: ($emojiBtn, emoji) ->
awardBlock = $emojiBtn
originalTitle = @getAwardTooltip awardBlock
authors = originalTitle.split ', '
authors.splice authors.indexOf('me'), 1
newAuthors = authors.join ', '
removeMeFromAuthorList: (emoji) ->
awardBlock = @findEmojiIcon(emoji).parent()
authors = awardBlock
.attr('data-original-title')
.split(', ')
authors.splice(authors.indexOf('me'),1)
awardBlock awardBlock
.closest('.js-emoji-btn') .closest '.js-emoji-btn'
.attr('data-original-title', authors.join(', ')) .removeData 'original-title'
.removeData 'title'
.attr 'data-original-title', newAuthors
.attr 'data-title', newAuthors
@resetTooltip(awardBlock) @resetTooltip(awardBlock)
addMeToAuthorList: (emoji) ->
addMeToUserList: (emoji) ->
awardBlock = @findEmojiIcon(emoji).parent() awardBlock = @findEmojiIcon(emoji).parent()
origTitle = awardBlock.attr('data-original-title').trim() origTitle = @getAwardTooltip awardBlock
authors = [] users = []
if origTitle if origTitle
authors = origTitle.split(', ') users = origTitle.trim().split(', ')
authors.push('me')
awardBlock.attr('data-original-title', authors.join(', ')) users.push('me')
awardBlock.attr('title', users.join(', '))
@resetTooltip(awardBlock) @resetTooltip(awardBlock)
resetTooltip: (award) -> resetTooltip: (award) ->
award.tooltip('destroy') award.tooltip('destroy')
# "destroy" call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout. # 'destroy' call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout.
setTimeout (-> setTimeout (->
award.tooltip() award.tooltip()
), 200 ), 200
createEmoji: (emoji) -> createEmoji_: (emoji) ->
emojiCssClass = @resolveNameToCssClass(emoji)
emojiCssClass = @resolveNameToCssClass emoji
nodes = []
nodes.push( buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'>
"<button class='btn award-control js-emoji-btn has-tooltip active' data-original-title='me'>", <div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>
"<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>", <span class='award-control-text js-counter'>1</span>
"<span class='award-control-text js-counter'>1</span>", </button>"
"</button>"
) emoji_node = $(buttonHtml)
.insertBefore '.js-awards-block .js-award-holder:not(.js-award-action-btn)'
$(nodes.join("\n")) .find '.emoji-icon'
.insertBefore('.js-award-holder') .data 'emoji', emoji
.find('.emoji-icon')
.data('emoji', emoji)
$('.award-control').tooltip() $('.award-control').tooltip()
$currentBlock = $ '.js-awards-block'
if $currentBlock.is '.hidden'
$currentBlock.removeClass 'hidden'
createEmoji: (emoji) ->
return @createEmoji_ emoji if $('.emoji-menu').length
awardMenuUrl = gl.awardMenuUrl or '/emojis'
@createEmojiMenu awardMenuUrl, => @createEmoji emoji
resolveNameToCssClass: (emoji) -> resolveNameToCssClass: (emoji) ->
emojiIcon = $(".emoji-menu-content [data-emoji='#{emoji}']")
if emojiIcon.length > 0 emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']")
unicodeName = emojiIcon.data('unicode-name')
if emoji_icon.length > 0
unicodeName = emoji_icon.data('unicode-name')
else else
# Find by alias # Find by alias
unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data('unicode-name') unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data('unicode-name')
"emoji-#{unicodeName}" return "emoji-#{unicodeName}"
postEmoji: (emoji, callback) ->
$.post @postEmojiUrl, { note: { postEmoji: (awardUrl, emoji, callback) ->
note: ":#{emoji}:" $.post awardUrl, { name: emoji }, (data) ->
noteable_type: @noteableType
noteable_id: @noteableId
}},(data) ->
if data.ok if data.ok
callback.call() callback.call()
findEmojiIcon: (emoji) -> findEmojiIcon: (emoji) ->
$(".awards > .js-emoji-btn [data-emoji='#{emoji}']") $(".js-awards-block.awards > .js-emoji-btn [data-emoji='#{emoji}']")
scrollToAwards: -> scrollToAwards: ->
$('body, html').animate({ $('body, html').animate({
scrollTop: $('.awards').offset().top - 80 scrollTop: $('.awards').offset().top - 80
}, 200) }, 200)
normilizeEmojiName: (emoji) ->
@aliases[emoji] || emoji
addEmojiToFrequentlyUsedList: (emoji) -> addEmojiToFrequentlyUsedList: (emoji) ->
frequentlyUsedEmojis = @getFrequentlyUsedEmojis() frequently_used_emojis = @getFrequentlyUsedEmojis()
frequentlyUsedEmojis.push(emoji) frequently_used_emojis.push(emoji)
$.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 }) $.cookie('frequently_used_emojis', frequently_used_emojis.join(','), { expires: 365 })
getFrequentlyUsedEmojis: -> getFrequentlyUsedEmojis: ->
frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(',') frequently_used_emojis = ($.cookie('frequently_used_emojis') || '').split(',')
_.compact(_.uniq(frequentlyUsedEmojis)) _.compact(_.uniq(frequently_used_emojis))
renderFrequentlyUsedBlock: -> renderFrequentlyUsedBlock: ->
if $.cookie('frequently_used_emojis') if $.cookie('frequently_used_emojis')
frequentlyUsedEmojis = @getFrequentlyUsedEmojis() frequently_used_emojis = @getFrequentlyUsedEmojis()
ul = $('<ul>') ul = $("<ul class='clearfix emoji-menu-list'>")
for emoji in frequentlyUsedEmojis for emoji in frequently_used_emojis
do (emoji) -> $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul)
$(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul)
$('input.emoji-search').after(ul).after($('<h5>').text('Frequently used')) $('input.emoji-search').after(ul).after($('<h5>').text('Frequently used'))
setupSearch: -> setupSearch: ->
$('input.emoji-search').keyup (ev) => $('input.emoji-search').on 'keyup', (ev) =>
term = $(ev.target).val() term = $(ev.target).val()
# Clean previous search results # Clean previous search results
...@@ -204,12 +303,12 @@ class @AwardsHandler ...@@ -204,12 +303,12 @@ class @AwardsHandler
if term if term
# Generate a search result block # Generate a search result block
h5 = $('<h5>').text('Search results').addClass('emoji-search') h5 = $('<h5>').text('Search results').addClass('emoji-search')
foundEmojis = @searchEmojis(term).show() found_emojis = @searchEmojis(term).show()
ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis) ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis)
$('.emoji-menu-content ul, .emoji-menu-content h5').hide() $('.emoji-menu-content ul, .emoji-menu-content h5').hide()
$('.emoji-menu-content').append(h5).append(ul) $('.emoji-menu-content').append(h5).append(ul)
else else
$('.emoji-menu-content').children().show() $('.emoji-menu-content').children().show()
searchEmojis: (term)-> searchEmojis: (term)->
$(".emoji-menu-content [data-emoji*='#{term}']").closest("li").clone() $(".emoji-menu-content [data-emoji*='#{term}']").closest('li').clone()
...@@ -17,11 +17,13 @@ class Dispatcher ...@@ -17,11 +17,13 @@ class Dispatcher
switch page switch page
when 'projects:issues:index' when 'projects:issues:index'
Issuable.init() Issuable.init()
new IssuableBulkActions()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
when 'projects:issues:show' when 'projects:issues:show'
new Issue() new Issue()
shortcut_handler = new ShortcutsIssuable() shortcut_handler = new ShortcutsIssuable()
new ZenMode() new ZenMode()
window.awardsHandler = new AwardsHandler()
when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone() new Milestone()
when 'dashboard:todos:index' when 'dashboard:todos:index'
...@@ -52,6 +54,7 @@ class Dispatcher ...@@ -52,6 +54,7 @@ class Dispatcher
new Diff() new Diff()
shortcut_handler = new ShortcutsIssuable(true) shortcut_handler = new ShortcutsIssuable(true)
new ZenMode() new ZenMode()
window.awardsHandler = new AwardsHandler()
when "projects:merge_requests:diffs" when "projects:merge_requests:diffs"
new Diff() new Diff()
new ZenMode() new ZenMode()
......
class @Flash class @Flash
constructor: (message, type)-> constructor: (message, type = 'alert')->
@flash = $(".flash-container") @flash = $(".flash-container")
@flash.html("") @flash.html("")
......
...@@ -11,6 +11,8 @@ class GitLabDropdownFilter ...@@ -11,6 +11,8 @@ class GitLabDropdownFilter
$inputContainer = @input.parent() $inputContainer = @input.parent()
$clearButton = $inputContainer.find('.js-dropdown-input-clear') $clearButton = $inputContainer.find('.js-dropdown-input-clear')
@indeterminateIds = []
# Clear click # Clear click
$clearButton.on 'click', (e) => $clearButton.on 'click', (e) =>
e.preventDefault() e.preventDefault()
...@@ -35,20 +37,20 @@ class GitLabDropdownFilter ...@@ -35,20 +37,20 @@ class GitLabDropdownFilter
if keyCode is 13 if keyCode is 13
return false return false
clearTimeout timeout # Only filter asynchronously only if option remote is set
timeout = setTimeout => if @options.remote
blur_field = @shouldBlur keyCode clearTimeout timeout
search_text = @input.val() timeout = setTimeout =>
blur_field = @shouldBlur keyCode
if blur_field and @filterInputBlur if blur_field and @filterInputBlur
@input.blur() @input.blur()
if @options.remote @options.query @input.val(), (data) =>
@options.query search_text, (data) =>
@options.callback(data) @options.callback(data)
else , 250
@filter search_text else
, 250 @filter @input.val()
shouldBlur: (keyCode) -> shouldBlur: (keyCode) ->
return BLUR_KEYCODES.indexOf(keyCode) >= 0 return BLUR_KEYCODES.indexOf(keyCode) >= 0
...@@ -142,6 +144,7 @@ class GitLabDropdown ...@@ -142,6 +144,7 @@ class GitLabDropdown
LOADING_CLASS = "is-loading" LOADING_CLASS = "is-loading"
PAGE_TWO_CLASS = "is-page-two" PAGE_TWO_CLASS = "is-page-two"
ACTIVE_CLASS = "is-active" ACTIVE_CLASS = "is-active"
INDETERMINATE_CLASS = "is-indeterminate"
currentIndex = -1 currentIndex = -1
FILTER_INPUT = '.dropdown-input .dropdown-input-field' FILTER_INPUT = '.dropdown-input .dropdown-input-field'
...@@ -182,9 +185,6 @@ class GitLabDropdown ...@@ -182,9 +185,6 @@ class GitLabDropdown
@fullData = data @fullData = data
@parseData @fullData @parseData @fullData
if @options.filterable
@filterInput.trigger 'keyup'
} }
# Init filterable # Init filterable
...@@ -298,6 +298,13 @@ class GitLabDropdown ...@@ -298,6 +298,13 @@ class GitLabDropdown
opened: => opened: =>
@addArrowKeyEvent() @addArrowKeyEvent()
if @options.setIndeterminateIds
@options.setIndeterminateIds.call(@)
# Makes indeterminate items effective
if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@parseData @fullData
contentHtml = $('.dropdown-content', @dropdown).html() contentHtml = $('.dropdown-content', @dropdown).html()
if @remote && contentHtml is "" if @remote && contentHtml is ""
@remote.execute() @remote.execute()
...@@ -309,12 +316,18 @@ class GitLabDropdown ...@@ -309,12 +316,18 @@ class GitLabDropdown
hidden: (e) => hidden: (e) =>
@removeArrayKeyEvent() @removeArrayKeyEvent()
$input = @dropdown.find(".dropdown-input-field")
if @options.filterable if @options.filterable
@dropdown $input
.find(".dropdown-input-field")
.blur() .blur()
.val("") .val("")
.trigger("keyup")
# Triggering 'keyup' will re-render the dropdown which is not always required
# specially if we want to keep the state of the dropdown needed for bulk-assignment
if not @options.persistWhenHide
$input.trigger("keyup")
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
...@@ -358,7 +371,7 @@ class GitLabDropdown ...@@ -358,7 +371,7 @@ class GitLabDropdown
if @options.renderRow if @options.renderRow
# Call the render function # Call the render function
html = @options.renderRow(data) html = @options.renderRow.call(@options, data, @)
else else
if not selected if not selected
value = if @options.id then @options.id(data) else data.id value = if @options.id then @options.id(data) else data.id
...@@ -443,6 +456,17 @@ class GitLabDropdown ...@@ -443,6 +456,17 @@ class GitLabDropdown
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel $(@el).find(".dropdown-toggle-text").text @options.toggleLabel
else else
selectedObject selectedObject
else if el.hasClass(INDETERMINATE_CLASS)
el.addClass ACTIVE_CLASS
el.removeClass INDETERMINATE_CLASS
if not value?
field.remove()
if not field.length and fieldName
@addInput(fieldName, value)
return selectedObject
else else
if not @options.multiSelect or el.hasClass('dropdown-clear-active') if not @options.multiSelect or el.hasClass('dropdown-clear-active')
@dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
...@@ -459,31 +483,42 @@ class GitLabDropdown ...@@ -459,31 +483,42 @@ class GitLabDropdown
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el) $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el)
if value? if value?
if !field.length and fieldName if !field.length and fieldName
# Create hidden input for form @addInput(fieldName, value)
input = "<input type='hidden' name='#{fieldName}' value='#{value}' />"
if @options.inputId?
input = $(input)
.attr('id', @options.inputId)
@dropdown.before input
else else
field.val value field.val value
return selectedObject return selectedObject
selectRowAtIndex: (index) -> addInput: (fieldName, value)->
selector = ".dropdown-content li:not(.divider):eq(#{index}) a" # Create hidden input for form
$input = $('<input>').attr('type', 'hidden')
.attr('name', fieldName)
.val(value)
if @options.inputId?
$input.attr('id', @options.inputId)
@dropdown.before $input
selectRowAtIndex: (e, index) ->
selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(#{index}) a"
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one #{selector}" selector = ".dropdown-page-one #{selector}"
# simulate a click on the first link # simulate a click on the first link
$(selector, @dropdown).trigger "click" $el = $(selector, @dropdown)
if $el.length
e.preventDefault()
e.stopImmediatePropagation()
$(selector, @dropdown)[0].click()
addArrowKeyEvent: -> addArrowKeyEvent: ->
ARROW_KEY_CODES = [38, 40] ARROW_KEY_CODES = [38, 40]
$input = @dropdown.find(".dropdown-input-field") $input = @dropdown.find(".dropdown-input-field")
selector = '.dropdown-content li:not(.divider)' selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator)'
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one #{selector}" selector = ".dropdown-page-one #{selector}"
...@@ -511,8 +546,8 @@ class GitLabDropdown ...@@ -511,8 +546,8 @@ class GitLabDropdown
return false return false
if currentKeyCode is 13 if currentKeyCode is 13 and currentIndex isnt -1
@selectRowAtIndex if currentIndex < 0 then 0 else currentIndex @selectRowAtIndex e, currentIndex
removeArrayKeyEvent: -> removeArrayKeyEvent: ->
$('body').off 'keydown' $('body').off 'keydown'
......
class @IssuableBulkActions
constructor: (opts = {}) ->
# Set defaults
{
@container = $('.content')
@form = @getElement('.bulk-update')
@issues = @getElement('.issues-list .issue')
} = opts
@bindEvents()
getElement: (selector) ->
@container.find selector
bindEvents: ->
@form.off('submit').on('submit', @onFormSubmit.bind(@))
onFormSubmit: (e) ->
e.preventDefault()
@submit()
submit: ->
_this = @
xhr = $.ajax
url: @form.attr 'action'
method: @form.attr 'method'
dataType: 'JSON',
data: @getFormDataAsObject()
xhr.done (response, status, xhr) ->
location.reload()
xhr.fail ->
new Flash("Issue update failed")
xhr.always @onFormSubmitAlways.bind(@)
onFormSubmitAlways: ->
@form.find('[type="submit"]').enable()
getSelectedIssues: ->
@issues.has('.selected_issue:checked')
getLabelsFromSelection: ->
labels = []
@getSelectedIssues().map ->
_labels = $(@).data('labels')
if _labels
_labels.map (labelId) ->
labels.push(labelId) if labels.indexOf(labelId) is -1
labels
###*
* Will return only labels that were marked previously and the user has unmarked
* @return {Array} Label IDs
###
getUnmarkedIndeterminedLabels: ->
result = []
labelsToKeep = []
for el in @getElement('.labels-filter .is-indeterminate')
labelsToKeep.push $(el).data('labelId')
for id in @getLabelsFromSelection()
# Only the ones that we are not going to keep
result.push(id) if labelsToKeep.indexOf(id) is -1
result
###*
* Simple form serialization, it will return just what we need
* Returns key/value pairs from form data
###
getFormDataAsObject: ->
formData =
update:
state_event : @form.find('input[name="update[state_event]"]').val()
assignee_id : @form.find('input[name="update[assignee_id]"]').val()
milestone_id : @form.find('input[name="update[milestone_id]"]').val()
issues_ids : @form.find('input[name="update[issues_ids]"]').val()
add_label_ids : []
remove_label_ids : []
@getLabelsToApply().map (id) ->
formData.update.add_label_ids.push id
@getLabelsToRemove().map (id) ->
formData.update.remove_label_ids.push id
formData
getLabelsToApply: ->
labelIds = []
$labels = @form.find('.labels-filter input[name="update[label_ids][]"]')
$labels.each (k, label) ->
labelIds.push $(label).val() if label
labelIds
###*
* Just an alias of @getUnmarkedIndeterminedLabels
* @return {Array} Array of labels
###
getLabelsToRemove: ->
@getUnmarkedIndeterminedLabels()
class @LabelsSelect class @LabelsSelect
constructor: -> constructor: ->
_this = @
$('.js-label-select').each (i, dropdown) -> $('.js-label-select').each (i, dropdown) ->
$dropdown = $(dropdown) $dropdown = $(dropdown)
projectId = $dropdown.data('project-id') projectId = $dropdown.data('project-id')
...@@ -196,10 +198,18 @@ class @LabelsSelect ...@@ -196,10 +198,18 @@ class @LabelsSelect
callback data callback data
renderRow: (label) -> renderRow: (label, instance) ->
removesAll = label.id is 0 or not label.id? $li = $('<li>')
$a = $('<a href="#">')
selectedClass = [] selectedClass = []
removesAll = label.id is 0 or not label.id?
if $dropdown.hasClass('js-filter-bulk-update')
indeterminate = instance.indeterminateIds
if indeterminate.indexOf(label.id) isnt -1
selectedClass.push 'is-indeterminate'
if $form.find("input[type='hidden']\ if $form.find("input[type='hidden']\
[name='#{$dropdown.data('fieldName')}']\ [name='#{$dropdown.data('fieldName')}']\
[value='#{this.id(label)}']").length [value='#{this.id(label)}']").length
...@@ -230,13 +240,17 @@ class @LabelsSelect ...@@ -230,13 +240,17 @@ class @LabelsSelect
else else
colorEl = '' colorEl = ''
"<li> # We need to identify which items are actually labels
<a href='#' class='#{selectedClass.join(' ')}'> if label.id
#{colorEl} selectedClass.push('label-item')
#{_.escape(label.title)} $a.attr('data-label-id', label.id)
</a>
</li>" $a.addClass(selectedClass.join(' '))
filterable: true .html("#{colorEl} #{_.escape(label.title)}")
# Return generated html
$li.html($a).prop('outerHTML')
persistWhenHide: $dropdown.data('persistWhenHide')
search: search:
fields: ['title'] fields: ['title']
selectable: true selectable: true
...@@ -280,10 +294,19 @@ class @LabelsSelect ...@@ -280,10 +294,19 @@ class @LabelsSelect
else if $dropdown.hasClass('js-filter-submit') else if $dropdown.hasClass('js-filter-submit')
$dropdown.closest('form').submit() $dropdown.closest('form').submit()
else else
saveLabelData() if not $dropdown.hasClass 'js-filter-bulk-update'
saveLabelData()
if $dropdown.hasClass('js-filter-bulk-update')
# If we are persisting state we need the classes
if not @options.persistWhenHide
$dropdown.parent().find('.is-active, .is-indeterminate').removeClass()
multiSelect: $dropdown.hasClass 'js-multiselect' multiSelect: $dropdown.hasClass 'js-multiselect'
clicked: (label) -> clicked: (label) ->
if $dropdown.hasClass('js-filter-bulk-update')
return
page = $('body').data 'page' page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index' isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is 'projects:merge_requests:index' isMRIndex = page is 'projects:merge_requests:index'
...@@ -298,4 +321,31 @@ class @LabelsSelect ...@@ -298,4 +321,31 @@ class @LabelsSelect
return return
else else
saveLabelData() saveLabelData()
setIndeterminateIds: ->
if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@indeterminateIds = _this.getIndeterminateIds()
) )
@bindEvents()
bindEvents: ->
$('body').on 'change', '.selected_issue', @onSelectCheckboxIssue
onSelectCheckboxIssue: ->
return if $('.selected_issue:checked').length
# Remove inputs
$('.issues_bulk_update .labels-filter input[type="hidden"]').remove()
# Also restore button text
$('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label')
getIndeterminateIds: ->
label_ids = []
$('.selected_issue:checked').each (i, el) ->
issue_id = $(el).data('id')
label_ids.push $("#issue_#{issue_id}").data('labels')
_.flatten(label_ids)
window.emojiAliases = ->
JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>')
...@@ -167,7 +167,7 @@ class @Notes ...@@ -167,7 +167,7 @@ class @Notes
return return
if note.award if note.award
awardsHandler.addAwardToEmojiBar(note.note) awardsHandler.addAwardToEmojiBar(note.name)
awardsHandler.scrollToAwards() awardsHandler.scrollToAwards()
# render note if it not present in loaded list # render note if it not present in loaded list
......
...@@ -156,11 +156,14 @@ class @SearchAutocomplete ...@@ -156,11 +156,14 @@ class @SearchAutocomplete
# No need to enable anything if user is not logged in # No need to enable anything if user is not logged in
return if !gon.current_user_id return if !gon.current_user_id
_this = @ unless @dropdown.hasClass('open')
@loadingSuggestions = false _this = @
@loadingSuggestions = false
@dropdown.addClass('open') @dropdown
@searchInput.removeClass('disabled') .addClass('open')
.trigger('shown.bs.dropdown')
@searchInput.removeClass('disabled')
onSearchInputKeyDown: => onSearchInputKeyDown: =>
# Saves last length of the entered text # Saves last length of the entered text
...@@ -191,7 +194,7 @@ class @SearchAutocomplete ...@@ -191,7 +194,7 @@ class @SearchAutocomplete
@disableAutocomplete() @disableAutocomplete()
else else
# We should display the menu only when input is not empty # We should display the menu only when input is not empty
@enableAutocomplete() @enableAutocomplete() if e.keyCode isnt KEYCODE.ENTER
@wrap.toggleClass 'has-value', !!e.target.value @wrap.toggleClass 'has-value', !!e.target.value
......
...@@ -232,9 +232,8 @@ ...@@ -232,9 +232,8 @@
a { a {
padding-left: 25px; padding-left: 25px;
&.is-active { &.is-indeterminate, &.is-active {
&::before { &::before {
content: "\f00c";
position: absolute; position: absolute;
left: 5px; left: 5px;
top: 50%; top: 50%;
...@@ -246,6 +245,14 @@ ...@@ -246,6 +245,14 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
} }
&.is-indeterminate::before {
content: "\f068";
}
&.is-active::before {
content: "\f00c";
}
} }
} }
......
...@@ -2,18 +2,10 @@ ...@@ -2,18 +2,10 @@
* Generic mixins * Generic mixins
*/ */
@mixin box-shadow($shadow) { @mixin box-shadow($shadow) {
-webkit-box-shadow: $shadow;
-moz-box-shadow: $shadow;
-ms-box-shadow: $shadow;
-o-box-shadow: $shadow;
box-shadow: $shadow; box-shadow: $shadow;
} }
@mixin border-radius($radius) { @mixin border-radius($radius) {
-webkit-border-radius: $radius;
-moz-border-radius: $radius;
-ms-border-radius: $radius;
-o-border-radius: $radius;
border-radius: $radius; border-radius: $radius;
} }
......
.awards { .awards {
line-height: 34px;
.emoji-icon { .emoji-icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
...@@ -9,8 +7,6 @@ ...@@ -9,8 +7,6 @@
.emoji-menu { .emoji-menu {
position: absolute; position: absolute;
top: 100%;
left: 0;
margin-top: 3px; margin-top: 3px;
z-index: 1000; z-index: 1000;
min-width: 160px; min-width: 160px;
...@@ -23,7 +19,12 @@ ...@@ -23,7 +19,12 @@
opacity: 0; opacity: 0;
transform: scale(.2); transform: scale(.2);
transform-origin: 0 -45px; transform-origin: 0 -45px;
transition: all .3s cubic-bezier(.87,-.41,.19,1.44); transition: .3s cubic-bezier(.87,-.41,.19,1.44);
transition-property: transform, opacity;
&.is-aligned-right {
transform-origin: 100% -45px;
}
&.is-visible { &.is-visible {
pointer-events: all; pointer-events: all;
...@@ -107,7 +108,7 @@ ...@@ -107,7 +108,7 @@
} }
&.is-loading { &.is-loading {
.award-control-icon { .award-control-icon-normal {
display: none; display: none;
} }
......
...@@ -3,12 +3,7 @@ ...@@ -3,12 +3,7 @@
background: #111; background: #111;
color: #fff; color: #fff;
font-family: $monospace_font; font-family: $monospace_font;
white-space: pre; white-space: pre-wrap;
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
overflow: auto; overflow: auto;
overflow-y: hidden; overflow-y: hidden;
font-size: 12px; font-size: 12px;
......
...@@ -158,13 +158,11 @@ ...@@ -158,13 +158,11 @@
.search-holder { .search-holder {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
display: -webkit-flex; display: -webkit-flex;
display: -ms-flexbox;
display: flex; display: flex;
} }
.search-field-holder { .search-field-holder {
-webkit-flex: 1 0 auto; -webkit-flex: 1 0 auto;
-ms-flex: 1 0 auto;
flex: 1 0 auto; flex: 1 0 auto;
position: relative; position: relative;
margin-right: 0; margin-right: 0;
......
module ToggleAwardEmoji
extend ActiveSupport::Concern
included do
before_action :authenticate_user!, only: [:toggle_award_emoji]
end
def toggle_award_emoji
name = params.require(:name)
awardable.toggle_award_emoji(name, current_user)
TodoService.new.new_award_emoji(awardable, current_user)
render json: { ok: true }
end
private
def awardable
raise NotImplementedError
end
end
class Projects::IssuesController < Projects::ApplicationController class Projects::IssuesController < Projects::ApplicationController
include ToggleSubscriptionAction include ToggleSubscriptionAction
include IssuableActions include IssuableActions
include ToggleAwardEmoji
before_action :module_enabled before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
...@@ -62,7 +63,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -62,7 +63,7 @@ class Projects::IssuesController < Projects::ApplicationController
def show def show
@note = @project.notes.new(noteable: @issue) @note = @project.notes.new(noteable: @issue)
@notes = @issue.notes.nonawards.with_associations.fresh @notes = @issue.notes.with_associations.fresh
@noteable = @issue @noteable = @issue
respond_to do |format| respond_to do |format|
...@@ -155,7 +156,12 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -155,7 +156,12 @@ class Projects::IssuesController < Projects::ApplicationController
def bulk_update def bulk_update
result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute
redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" })
respond_to do |format|
format.json do
render json: { notice: "#{result[:count]} issues updated" }
end
end
end end
protected protected
...@@ -169,6 +175,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -169,6 +175,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
alias_method :subscribable_resource, :issue alias_method :subscribable_resource, :issue
alias_method :issuable, :issue alias_method :issuable, :issue
alias_method :awardable, :issue
def authorize_read_issue! def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue) return render_404 unless can?(current_user, :read_issue, @issue)
...@@ -214,7 +221,10 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -214,7 +221,10 @@ class Projects::IssuesController < Projects::ApplicationController
:issues_ids, :issues_ids,
:assignee_id, :assignee_id,
:milestone_id, :milestone_id,
:state_event :state_event,
label_ids: [],
add_label_ids: [],
remove_label_ids: []
) )
end end
end end
...@@ -2,6 +2,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -2,6 +2,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
include ToggleSubscriptionAction include ToggleSubscriptionAction
include DiffHelper include DiffHelper
include IssuableActions include IssuableActions
include ToggleAwardEmoji
before_action :module_enabled before_action :module_enabled
before_action :merge_request, only: [ before_action :merge_request, only: [
...@@ -201,7 +202,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -201,7 +202,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
if params[:merge_when_build_succeeds].present? && @merge_request.pipeline && @merge_request.pipeline.active? if params[:merge_when_build_succeeds].present? && @merge_request.pipeline && @merge_request.pipeline.active?
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
.execute(@merge_request) .execute(@merge_request)
@status = :merge_when_build_succeeds @status = :merge_when_build_succeeds
else else
MergeWorker.perform_async(@merge_request.id, current_user.id, params) MergeWorker.perform_async(@merge_request.id, current_user.id, params)
...@@ -270,6 +271,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -270,6 +271,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
alias_method :subscribable_resource, :merge_request alias_method :subscribable_resource, :merge_request
alias_method :issuable, :merge_request alias_method :issuable, :merge_request
alias_method :awardable, :merge_request
def closes_issues def closes_issues
@closes_issues ||= @merge_request.closes_issues @closes_issues ||= @merge_request.closes_issues
...@@ -305,7 +307,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -305,7 +307,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_show_vars def define_show_vars
# Build a note object for comment form # Build a note object for comment form
@note = @project.notes.new(noteable: @merge_request) @note = @project.notes.new(noteable: @merge_request)
@notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh @notes = @merge_request.mr_and_commit_notes.inc_author.fresh
@discussions = @notes.discussions @discussions = @notes.discussions
@noteable = @merge_request @noteable = @merge_request
......
...@@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_read_note! before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create] before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy] before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :find_current_user_notes, except: [:destroy, :delete_attachment, :award_toggle] before_action :find_current_user_notes, only: [:index]
def index def index
current_fetched_at = Time.now.to_i current_fetched_at = Time.now.to_i
...@@ -56,30 +56,6 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -56,30 +56,6 @@ class Projects::NotesController < Projects::ApplicationController
end end
end end
def award_toggle
noteable = if note_params[:noteable_type] == "issue"
project.issues.find(note_params[:noteable_id])
else
project.merge_requests.find(note_params[:noteable_id])
end
data = {
author: current_user,
is_award: true,
note: note_params[:note].delete(":")
}
note = noteable.notes.find_by(data)
if note
note.destroy
else
Notes::CreateService.new(project, current_user, note_params).execute
end
render json: { ok: true }
end
private private
def note def note
...@@ -131,13 +107,20 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -131,13 +107,20 @@ class Projects::NotesController < Projects::ApplicationController
end end
def note_json(note) def note_json(note)
if note.valid? if note.is_a?(AwardEmoji)
{
valid: note.valid?,
award: true,
id: note.id,
name: note.name
}
elsif note.valid?
{ {
valid: true, valid: true,
id: note.id, id: note.id,
discussion_id: note.discussion_id, discussion_id: note.discussion_id,
html: note_to_html(note), html: note_to_html(note),
award: note.is_award, award: false,
note: note.note, note: note.note,
discussion_html: note_to_discussion_html(note), discussion_html: note_to_discussion_html(note),
discussion_with_diff_html: note_to_discussion_with_diff_html(note) discussion_with_diff_html: note_to_discussion_with_diff_html(note)
...@@ -145,7 +128,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -145,7 +128,7 @@ class Projects::NotesController < Projects::ApplicationController
else else
{ {
valid: false, valid: false,
award: note.is_award, award: false,
errors: note.errors errors: note.errors
} }
end end
......
...@@ -139,7 +139,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -139,7 +139,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: AwardEmoji.urls, emojis: Gitlab::AwardEmoji.urls,
issues: autocomplete.issues, issues: autocomplete.issues,
milestones: autocomplete.milestones, milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests, mergerequests: autocomplete.merge_requests,
......
...@@ -12,9 +12,9 @@ class NotesFinder ...@@ -12,9 +12,9 @@ class NotesFinder
when "commit" when "commit"
project.notes.for_commit_id(target_id).non_diff_notes project.notes.for_commit_id(target_id).non_diff_notes
when "issue" when "issue"
project.issues.find(target_id).notes.nonawards.inc_author project.issues.find(target_id).notes.inc_author
when "merge_request" when "merge_request"
project.merge_requests.find(target_id).mr_and_commit_notes.nonawards.inc_author project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
when "snippet", "project_snippet" when "snippet", "project_snippet"
project.snippets.find(target_id).notes project.snippets.find(target_id).notes
else else
......
...@@ -96,5 +96,4 @@ module IssuablesHelper ...@@ -96,5 +96,4 @@ module IssuablesHelper
issuable.open? ? :opened : :closed issuable.open? ? :opened : :closed
end end
end end
end end
...@@ -145,16 +145,14 @@ module IssuesHelper ...@@ -145,16 +145,14 @@ module IssuesHelper
end end
end end
def emoji_author_list(notes, current_user) def award_user_list(awards, current_user)
list = notes.map do |note| awards.map do |award|
note.author == current_user ? "me" : note.author.name award.user == current_user ? 'me' : award.user.name
end end.join(', ')
list.join(", ")
end end
def note_active_class(notes, current_user) def award_active_class(awards, current_user)
if current_user && notes.pluck(:author_id).include?(current_user.id) if current_user && awards.find { |a| a.user_id == current_user.id }
"active" "active"
else else
"" ""
......
class AwardEmoji < ActiveRecord::Base
DOWNVOTE_NAME = "thumbsdown".freeze
UPVOTE_NAME = "thumbsup".freeze
include Participable
belongs_to :awardable, polymorphic: true
belongs_to :user
validates :awardable, :user, presence: true
validates :name, presence: true, inclusion: { in: Emoji.emojis_names }
validates :name, uniqueness: { scope: [:user, :awardable_type, :awardable_id] }
participant :user
scope :downvotes, -> { where(name: DOWNVOTE_NAME) }
scope :upvotes, -> { where(name: UPVOTE_NAME) }
def downvote?
self.name == DOWNVOTE_NAME
end
def upvote?
self.name == UPVOTE_NAME
end
end
module Awardable
extend ActiveSupport::Concern
included do
has_many :award_emoji, as: :awardable, dependent: :destroy
if self < Participable
participant :award_emoji
end
end
module ClassMethods
def order_upvotes_desc
order_votes_desc(AwardEmoji::UPVOTE_NAME)
end
def order_downvotes_desc
order_votes_desc(AwardEmoji::DOWNVOTE_NAME)
end
def order_votes_desc(emoji_name)
awardable_table = self.arel_table
awards_table = AwardEmoji.arel_table
join_clause = awardable_table.join(awards_table, Arel::Nodes::OuterJoin).on(
awards_table[:awardable_id].eq(awardable_table[:id]).and(
awards_table[:awardable_type].eq(self.name).and(
awards_table[:name].eq(emoji_name)
)
)
).join_sources
joins(join_clause).group(awardable_table[:id]).reorder("COUNT(award_emoji.id) DESC")
end
end
def grouped_awards(with_thumbs: true)
awards = award_emoji.group_by(&:name)
if with_thumbs
awards[AwardEmoji::UPVOTE_NAME] ||= []
awards[AwardEmoji::DOWNVOTE_NAME] ||= []
end
awards
end
def downvotes
award_emoji.downvotes.count
end
def upvotes
award_emoji.upvotes.count
end
def emoji_awardable?
true
end
def awarded_emoji?(emoji_name, current_user)
award_emoji.where(name: emoji_name, user: current_user).exists?
end
def create_award_emoji(name, current_user)
return unless emoji_awardable?
award_emoji.create(name: name, user: current_user)
end
def remove_award_emoji(name, current_user)
award_emoji.where(name: name, user: current_user).destroy_all
end
def toggle_award_emoji(emoji_name, current_user)
if awarded_emoji?(emoji_name, current_user)
remove_award_emoji(emoji_name, current_user)
else
create_award_emoji(emoji_name, current_user)
end
end
end
...@@ -10,6 +10,7 @@ module Issuable ...@@ -10,6 +10,7 @@ module Issuable
include Mentionable include Mentionable
include Subscribable include Subscribable
include StripAttribute include StripAttribute
include Awardable
included do included do
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
...@@ -115,29 +116,6 @@ module Issuable ...@@ -115,29 +116,6 @@ module Issuable
end end
end end
def order_downvotes_desc
order_votes_desc('thumbsdown')
end
def order_upvotes_desc
order_votes_desc('thumbsup')
end
def order_votes_desc(award_emoji_name)
issuable_table = self.arel_table
note_table = Note.arel_table
join_clause = issuable_table.join(note_table, Arel::Nodes::OuterJoin).on(
note_table[:noteable_id].eq(issuable_table[:id]).and(
note_table[:noteable_type].eq(self.name).and(
note_table[:is_award].eq(true).and(note_table[:note].eq(award_emoji_name))
)
)
).join_sources
joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC")
end
def with_label(title, sort = nil) def with_label(title, sort = nil)
if title.is_a?(Array) && title.size > 1 if title.is_a?(Array) && title.size > 1
joins(:labels).where(labels: { title: title }).group(*grouping_columns(sort)).having("COUNT(DISTINCT labels.title) = #{title.size}") joins(:labels).where(labels: { title: title }).group(*grouping_columns(sort)).having("COUNT(DISTINCT labels.title) = #{title.size}")
...@@ -179,14 +157,6 @@ module Issuable ...@@ -179,14 +157,6 @@ module Issuable
opened? || reopened? opened? || reopened?
end end
def downvotes
notes.awards.where(note: "thumbsdown").count
end
def upvotes
notes.awards.where(note: "thumbsup").count
end
def user_notes_count def user_notes_count
notes.user.count notes.user.count
end end
......
...@@ -110,6 +110,10 @@ class LegacyDiffNote < Note ...@@ -110,6 +110,10 @@ class LegacyDiffNote < Note
@active @active
end end
def award_emoji_supported?
false
end
private private
def find_diff def find_diff
......
...@@ -21,11 +21,8 @@ class Note < ActiveRecord::Base ...@@ -21,11 +21,8 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true delegate :name, :email, to: :author, prefix: true
delegate :title, to: :noteable, allow_nil: true delegate :title, to: :noteable, allow_nil: true
before_validation :set_award!
validates :note, :project, presence: true validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
validates :note, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award }
# Attachments are deprecated and are handled by Markdown uploader # Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size } validates :attachment, file_size: { maximum: :max_attachment_size }
...@@ -43,8 +40,6 @@ class Note < ActiveRecord::Base ...@@ -43,8 +40,6 @@ class Note < ActiveRecord::Base
mount_uploader :attachment, AttachmentUploader mount_uploader :attachment, AttachmentUploader
# Scopes # Scopes
scope :awards, ->{ where(is_award: true) }
scope :nonawards, ->{ where(is_award: false) }
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :system, ->{ where(system: true) } scope :system, ->{ where(system: true) }
scope :user, ->{ where(system: false) } scope :user, ->{ where(system: false) }
...@@ -109,19 +104,6 @@ class Note < ActiveRecord::Base ...@@ -109,19 +104,6 @@ class Note < ActiveRecord::Base
found_notes.where('issues.confidential IS NULL OR issues.confidential IS FALSE') found_notes.where('issues.confidential IS NULL OR issues.confidential IS FALSE')
end end
end end
def grouped_awards
notes = {}
awards.select(:note).distinct.map do |note|
notes[note.note] = where(note: note.note)
end
notes["thumbsup"] ||= Note.none
notes["thumbsdown"] ||= Note.none
notes
end
end end
def cross_reference? def cross_reference?
...@@ -205,44 +187,24 @@ class Note < ActiveRecord::Base ...@@ -205,44 +187,24 @@ class Note < ActiveRecord::Base
Event.reset_event_cache_for(self) Event.reset_event_cache_for(self)
end end
def downvote?
is_award && note == "thumbsdown"
end
def upvote?
is_award && note == "thumbsup"
end
def editable? def editable?
!system? && !is_award !system?
end end
def cross_reference_not_visible_for?(user) def cross_reference_not_visible_for?(user)
cross_reference? && referenced_mentionables(user).empty? cross_reference? && referenced_mentionables(user).empty?
end end
# Checks if note is an award added as a comment def award_emoji?
# award_emoji_supported? && contains_emoji_only?
# If note is an award, this method sets is_award to true
# and changes content of the note to award name.
#
# Method is executed as a before_validation callback.
#
def set_award!
return unless awards_supported? && contains_emoji_only?
self.is_award = true
self.note = award_emoji_name
end end
private
def clear_blank_line_code! def clear_blank_line_code!
self.line_code = nil if self.line_code.blank? self.line_code = nil if self.line_code.blank?
end end
def awards_supported? def award_emoji_supported?
(for_issue? || for_merge_request?) && !diff_note? noteable.is_a?(Awardable)
end end
def contains_emoji_only? def contains_emoji_only?
...@@ -251,6 +213,6 @@ class Note < ActiveRecord::Base ...@@ -251,6 +213,6 @@ class Note < ActiveRecord::Base
def award_emoji_name def award_emoji_name
original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1] original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
AwardEmoji.normilize_emoji_name(original_name) Gitlab::AwardEmoji.normalize_emoji_name(original_name)
end end
end end
...@@ -83,7 +83,7 @@ class IrkerService < Service ...@@ -83,7 +83,7 @@ class IrkerService < Service
self.channels = recipients.split(/\s+/).map do |recipient| self.channels = recipients.split(/\s+/).map do |recipient|
format_channel(recipient) format_channel(recipient)
end end
channels.reject! &:nil? channels.reject!(&:nil?)
end end
def format_channel(recipient) def format_channel(recipient)
......
...@@ -84,6 +84,7 @@ class User < ActiveRecord::Base ...@@ -84,6 +84,7 @@ class User < ActiveRecord::Base
has_many :builds, dependent: :nullify, class_name: 'Ci::Build' has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
has_many :todos, dependent: :destroy has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy has_many :notification_settings, dependent: :destroy
has_many :award_emoji, as: :awardable, dependent: :destroy
# #
# Validations # Validations
......
...@@ -45,6 +45,8 @@ class IssuableBaseService < BaseService ...@@ -45,6 +45,8 @@ class IssuableBaseService < BaseService
unless can?(current_user, ability, project) unless can?(current_user, ability, project)
params.delete(:milestone_id) params.delete(:milestone_id)
params.delete(:add_label_ids)
params.delete(:remove_label_ids)
params.delete(:label_ids) params.delete(:label_ids)
params.delete(:assignee_id) params.delete(:assignee_id)
end end
...@@ -67,10 +69,34 @@ class IssuableBaseService < BaseService ...@@ -67,10 +69,34 @@ class IssuableBaseService < BaseService
end end
def filter_labels def filter_labels
return if params[:label_ids].to_a.empty? if params[:add_label_ids].present? || params[:remove_label_ids].present?
params.delete(:label_ids)
filter_labels_in_param(:add_label_ids)
filter_labels_in_param(:remove_label_ids)
else
filter_labels_in_param(:label_ids)
end
end
def filter_labels_in_param(key)
return if params[key].to_a.empty?
params[:label_ids] = params[key] = project.labels.where(id: params[key]).pluck(:id)
project.labels.where(id: params[:label_ids]).pluck(:id) end
def update_issuable(issuable, attributes)
issuable.with_transaction_returning_status do
add_label_ids = attributes.delete(:add_label_ids)
remove_label_ids = attributes.delete(:remove_label_ids)
issuable.label_ids |= add_label_ids if add_label_ids
issuable.label_ids -= remove_label_ids if remove_label_ids
issuable.assign_attributes(attributes.merge(updated_by: current_user))
issuable.save
end
end end
def update(issuable) def update(issuable)
...@@ -78,7 +104,7 @@ class IssuableBaseService < BaseService ...@@ -78,7 +104,7 @@ class IssuableBaseService < BaseService
filter_params filter_params
old_labels = issuable.labels.to_a old_labels = issuable.labels.to_a
if params.present? && issuable.update_attributes(params.merge(updated_by: current_user)) if params.present? && update_issuable(issuable, params)
issuable.reset_events_cache issuable.reset_events_cache
handle_common_system_notes(issuable, old_labels: old_labels) handle_common_system_notes(issuable, old_labels: old_labels)
handle_changes(issuable, old_labels: old_labels) handle_changes(issuable, old_labels: old_labels)
......
...@@ -4,9 +4,9 @@ module Issues ...@@ -4,9 +4,9 @@ module Issues
issues_ids = params.delete(:issues_ids).split(",") issues_ids = params.delete(:issues_ids).split(",")
issue_params = params issue_params = params
issue_params.delete(:state_event) unless issue_params[:state_event].present? %i(state_event milestone_id assignee_id add_label_ids remove_label_ids).each do |key|
issue_params.delete(:milestone_id) unless issue_params[:milestone_id].present? issue_params.delete(key) unless issue_params[key].present?
issue_params.delete(:assignee_id) unless issue_params[:assignee_id].present? end
issues = Issue.where(id: issues_ids) issues = Issue.where(id: issues_ids)
issues.each do |issue| issues.each do |issue|
......
...@@ -24,6 +24,7 @@ module Issues ...@@ -24,6 +24,7 @@ module Issues
@new_issue = create_new_issue @new_issue = create_new_issue
rewrite_notes rewrite_notes
rewrite_award_emoji
add_note_moved_from add_note_moved_from
# Old issue tasks # Old issue tasks
...@@ -72,6 +73,14 @@ module Issues ...@@ -72,6 +73,14 @@ module Issues
end end
end end
def rewrite_award_emoji
@old_issue.award_emoji.each do |award|
new_award = award.dup
new_award.awardable = @new_issue
new_award.save
end
end
def rewrite_content(content) def rewrite_content(content)
return unless content return unless content
......
...@@ -5,6 +5,13 @@ module Notes ...@@ -5,6 +5,13 @@ module Notes
note.author = current_user note.author = current_user
note.system = false note.system = false
if note.award_emoji?
noteable = note.noteable
todo_service.new_award_emoji(noteable, current_user)
return noteable.create_award_emoji(note.award_emoji_name, current_user)
end
if note.save if note.save
# Finish the harder work in the background # Finish the harder work in the background
NewNoteWorker.perform_in(2.seconds, note.id, params) NewNoteWorker.perform_in(2.seconds, note.id, params)
......
...@@ -8,7 +8,7 @@ module Notes ...@@ -8,7 +8,7 @@ module Notes
def execute def execute
# Skip system notes, like status changes and cross-references and awards # Skip system notes, like status changes and cross-references and awards
unless @note.system || @note.is_award unless @note.system?
EventCreateService.new.leave_note(@note, @note.author) EventCreateService.new.leave_note(@note, @note.author)
@note.create_cross_references! @note.create_cross_references!
execute_note_hooks execute_note_hooks
......
...@@ -130,8 +130,7 @@ class NotificationService ...@@ -130,8 +130,7 @@ class NotificationService
# ignore gitlab service messages # ignore gitlab service messages
return true if note.note.start_with?('Status changed to closed') return true if note.note.start_with?('Status changed to closed')
return true if note.cross_reference? && note.system == true return true if note.cross_reference? && note.system?
return true if note.is_award
target = note.noteable target = note.noteable
......
...@@ -22,6 +22,7 @@ module Oauth2::AccessTokenValidationService ...@@ -22,6 +22,7 @@ module Oauth2::AccessTokenValidationService
end end
protected protected
# True if the token's scope is a superset of required scopes, # True if the token's scope is a superset of required scopes,
# or the required scopes is empty. # or the required scopes is empty.
def sufficient_scope?(token, scopes) def sufficient_scope?(token, scopes)
......
...@@ -122,6 +122,14 @@ class TodoService ...@@ -122,6 +122,14 @@ class TodoService
handle_note(note, current_user) handle_note(note, current_user)
end end
# When an emoji is awarded we should:
#
# * mark all pending todos related to the awardable for the current user as done
#
def new_award_emoji(awardable, current_user)
mark_pending_todos_as_done(awardable, current_user)
end
# When marking pending todos as done we should: # When marking pending todos as done we should:
# #
# * mark all pending todos related to the target for the current user as done # * mark all pending todos related to the target for the current user as done
......
- grouped_emojis = awardable.grouped_awards(with_thumbs: inline)
.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } }
- awards_sort(grouped_emojis).each do |emoji, awards|
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } }
= emoji_icon(emoji, sprite: false)
%span.award-control-text.js-counter
= awards.count
- if current_user
:javascript
gl.awardMenuUrl = "#{emojis_path}"
.award-menu-holder.js-award-holder
%button.btn.award-control.js-add-award{ type: "button", data: { award_menu_url: emojis_path } }
= icon('smile-o', class: "award-control-icon award-control-icon-normal")
= icon('spinner spin', class: "award-control-icon award-control-icon-loading")
%span.award-control-text
Add
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
.login-body .login-body
= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
= f.hidden_field :remember_me, value: params[resource_name][:remember_me] = f.hidden_field :remember_me, value: params[resource_name][:remember_me]
= f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor Authentication code', required: true, autofocus: true = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor Authentication code', required: true, autofocus: true, autocomplete: 'off'
%p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
.prepend-top-20 .prepend-top-20
= f.submit "Verify code", class: "btn btn-save" = f.submit "Verify code", class: "btn btn-save"
.emoji-menu .emoji-menu
.emoji-menu-content .emoji-menu-content
= text_field_tag :emoji_search, "", class: "emoji-search search-input form-control" = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
- AwardEmoji.emoji_by_category.each do |category, emojis| - Gitlab::AwardEmoji.emoji_by_category.each do |category, emojis|
%h5.emoji-menu-title %h5.emoji-menu-title
= AwardEmoji::CATEGORIES[category] = Gitlab::AwardEmoji::CATEGORIES[category]
%ul.clearfix.emoji-menu-list %ul.clearfix.emoji-menu-list
- emojis.each do |emoji| - emojis.each do |emoji|
%li.pull-left.text-center.emoji-menu-list-item %li.pull-left.text-center.emoji-menu-list-item
......
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
= link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do = link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do
= icon('hdd-o fw') = icon('hdd-o fw')
%span %span
Container Registry Registry
- if project_nav_tab? :graphs - if project_nav_tab? :graphs
= nav_link(controller: %w(graphs)) do = nav_link(controller: %w(graphs)) do
......
%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue) } %li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } }
- if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project) - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project)
.issue-check .issue-check
= check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
= icon('thumbs-down') = icon('thumbs-down')
= downvotes = downvotes
- note_count = issue.notes.user.nonawards.count - note_count = issue.notes.user.count
%li %li
= link_to issue_path(issue, anchor: 'notes'), class: ('issue-no-comments' if note_count.zero?) do = link_to issue_path(issue, anchor: 'notes'), class: ('issue-no-comments' if note_count.zero?) do
= icon('comments') = icon('comments')
......
...@@ -68,9 +68,9 @@ ...@@ -68,9 +68,9 @@
#related-branches{ data: { url: related_branches_namespace_project_issue_url(@project.namespace, @project, @issue) } } #related-branches{ data: { url: related_branches_namespace_project_issue_url(@project.namespace, @project, @issue) } }
// This element is filled in using JavaScript. // This element is filled in using JavaScript.
.content-block.content-block-small .content-block.content-block-small
= render 'new_branch' = render 'new_branch'
= render 'votes/votes_block', votable: @issue = render 'award_emoji/awards_block', awardable: @issue, inline: true
%section.issuable-discussion %section.issuable-discussion
= render 'projects/issues/discussion' = render 'projects/issues/discussion'
......
%li{id: dom_id(label)} %li{ id: dom_id(label), data: { id: label.id } }
= render "shared/label_row", label: label = render "shared/label_row", label: label
.pull-info-right .pull-info-right
%span.append-right-20 %span.append-right-20
= link_to_label(label, type: :merge_request) do = link_to_label(label, type: :merge_request) do
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
= icon('thumbs-down') = icon('thumbs-down')
= downvotes = downvotes
- note_count = merge_request.mr_and_commit_notes.user.nonawards.count - note_count = merge_request.mr_and_commit_notes.user.count
%li %li
= link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do
= icon('comments') = icon('comments')
......
...@@ -6,4 +6,3 @@ ...@@ -6,4 +6,3 @@
- if @merge_requests.present? - if @merge_requests.present?
= paginate @merge_requests, theme: "gitlab" = paginate @merge_requests, theme: "gitlab"
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
%li.notes-tab %li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do
Discussion Discussion
%span.badge= @merge_request.mr_and_commit_notes.user.nonawards.count %span.badge= @merge_request.mr_and_commit_notes.user.count
%li.commits-tab %li.commits-tab
= link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
Commits Commits
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
.tab-content .tab-content
#notes.notes.tab-pane.voting_notes #notes.notes.tab-pane.voting_notes
.content-block.content-block-small.oneline-block .content-block.content-block-small.oneline-block
= render 'votes/votes_block', votable: @merge_request = render 'award_emoji/awards_block', awardable: @merge_request, inline: true
.row .row
%section.col-md-12 %section.col-md-12
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
- if controller.controller_name == 'issues' - if controller.controller_name == 'issues'
.issues_bulk_update.hide .issues_bulk_update.hide
= form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post, class: 'bulk-update' do
.filter-item.inline .filter-item.inline
= dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
%ul %ul
...@@ -44,6 +44,10 @@ ...@@ -44,6 +44,10 @@
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 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 } }) = 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 } })
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, show_footer: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, 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
......
- show_create = local_assigns.fetch(:show_create, true)
- extra_options = local_assigns.fetch(:extra_options, true)
- filter_submit = local_assigns.fetch(:filter_submit, true)
- show_footer = local_assigns.fetch(:show_footer, true)
- data_options = local_assigns.fetch(:data_options, {})
- classes = local_assigns.fetch(:classes, [])
- dropdown_data = {toggle: 'dropdown', field_name: 'label_name[]', show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}
- dropdown_data.merge!(data_options)
- classes << 'js-extra-options' if extra_options
- classes << 'js-filter-submit' if filter_submit
- if params[:label_name].present? - if params[:label_name].present?
- if params[:label_name].respond_to?('any?') - if params[:label_name].respond_to?('any?')
- params[:label_name].each do |label| - params[:label_name].each do |label|
= hidden_field_tag "label_name[]", label, id: nil = hidden_field_tag "label_name[]", label, id: nil
.dropdown .dropdown
%button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-multiselect.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name[]", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data}
%span.dropdown-toggle-text %span.dropdown-toggle-text
= h(multi_label_name(params[:label_name], "Label")) = h(multi_label_name(params[:label_name], "Label"))
= icon('chevron-down') = icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label" } = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label", show_footer: show_footer, show_create: show_create }
- if can? current_user, :admin_label, @project and @project - if show_create and @project and can?(current_user, :admin_label, @project)
= render partial: "shared/issuable/label_page_create" = render partial: "shared/issuable/label_page_create"
= dropdown_loading = dropdown_loading
- title = local_assigns.fetch(:title, 'Assign labels') - title = local_assigns.fetch(:title, 'Assign labels')
- show_create = local_assigns.fetch(:show_create, true)
- show_footer = local_assigns.fetch(:show_footer, true)
- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels') - filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels')
.dropdown-page-one .dropdown-page-one
= dropdown_title(title) = dropdown_title(title)
= dropdown_filter(filter_placeholder) = dropdown_filter(filter_placeholder)
= dropdown_content = dropdown_content
- if @project - if @project && show_footer
= dropdown_footer do = dropdown_footer do
%ul.dropdown-footer-list %ul.dropdown-footer-list
- if can? current_user, :admin_label, @project - if can?(current_user, :admin_label, @project)
%li %li
%a.dropdown-toggle-page{href: "#"} %a.dropdown-toggle-page{href: "#"}
Create new Create new
%li %li
= link_to namespace_project_labels_path(@project.namespace, @project), :"data-is-link" => true do = link_to namespace_project_labels_path(@project.namespace, @project), :"data-is-link" => true do
- if can? current_user, :admin_label, @project - if show_create && @project && can?(current_user, :admin_label, @project)
Manage labels Manage labels
- else - else
View labels View labels
= dropdown_loading = dropdown_loading
\ No newline at end of file
.awards.votes-block
- awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes|
%button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(notes, current_user)), data: {placement: "top", original_title: emoji_author_list(notes, current_user)}}
= emoji_icon(emoji, sprite: false)
%span.award-control-text.js-counter
= notes.count
- if current_user
%div.award-menu-holder.js-award-holder
%a.btn.award-control.js-add-award{"href" => "#"}
= icon('smile-o', {class: "award-control-icon"})
= icon('spinner spin', {class: "award-control-icon award-control-icon-loading"})
%span.award-control-text
Add
- if current_user
:javascript
var getEmojisUrl = "#{emojis_path}";
var postEmojiUrl = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}";
var noteableType = "#{votable.class.name.underscore}";
var noteableId = "#{votable.id}";
var unicodes = #{AwardEmoji.unicode.to_json};
window.awardsHandler = new AwardsHandler(
getEmojisUrl,
postEmojiUrl,
noteableType,
noteableId,
unicodes
);
...@@ -8,3 +8,7 @@ ...@@ -8,3 +8,7 @@
# inflect.irregular 'person', 'people' # inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep ) # inflect.uncountable %w( fish sheep )
# end # end
#
ActiveSupport::Inflector.inflections do |inflect|
inflect.uncountable %w(award_emoji)
end
...@@ -652,6 +652,7 @@ Rails.application.routes.draw do ...@@ -652,6 +652,7 @@ Rails.application.routes.draw do
post :cancel_merge_when_build_succeeds post :cancel_merge_when_build_succeeds
get :ci_status get :ci_status
post :toggle_subscription post :toggle_subscription
post :toggle_award_emoji
post :remove_wip post :remove_wip
end end
...@@ -727,6 +728,7 @@ Rails.application.routes.draw do ...@@ -727,6 +728,7 @@ Rails.application.routes.draw do
resources :issues, constraints: { id: /\d+/ } do resources :issues, constraints: { id: /\d+/ } do
member do member do
post :toggle_subscription post :toggle_subscription
post :toggle_award_emoji
get :referenced_merge_requests get :referenced_merge_requests
get :related_branches get :related_branches
get :can_create_branch get :can_create_branch
...@@ -757,10 +759,6 @@ Rails.application.routes.draw do ...@@ -757,10 +759,6 @@ Rails.application.routes.draw do
member do member do
delete :delete_attachment delete :delete_attachment
end end
collection do
post :award_toggle
end
end end
resources :uploads, only: [:create] do resources :uploads, only: [:create] do
......
class AddAwardEmoji < ActiveRecord::Migration
def change
create_table :award_emoji do |t|
t.string :name
t.references :user
t.references :awardable, polymorphic: true
t.timestamps
end
add_index :award_emoji, :user_id
add_index :award_emoji, [:awardable_type, :awardable_id]
end
end
class ConvertAwardNoteToEmojiAward < ActiveRecord::Migration
def change
def up
execute "INSERT INTO award_emoji (awardable_type, awardable_id, user_id, name, created_at, updated_at) (SELECT noteable_type, noteable_id, author_id, note, created_at, updated_at FROM notes WHERE is_award = true)"
execute "DELETE FROM notes WHERE is_award = true"
end
end
end
class RemoveNoteIsAward < ActiveRecord::Migration
def change
remove_column :notes, :is_award, :boolean
end
end
...@@ -100,6 +100,18 @@ ActiveRecord::Schema.define(version: 20160530150109) do ...@@ -100,6 +100,18 @@ ActiveRecord::Schema.define(version: 20160530150109) do
add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree
add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree
create_table "award_emoji", force: :cascade do |t|
t.string "name"
t.integer "user_id"
t.integer "awardable_id"
t.string "awardable_type"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "award_emoji", ["awardable_type", "awardable_id"], name: "index_award_emoji_on_awardable_type_and_awardable_id", using: :btree
add_index "award_emoji", ["user_id"], name: "index_award_emoji_on_user_id", using: :btree
create_table "broadcast_messages", force: :cascade do |t| create_table "broadcast_messages", force: :cascade do |t|
t.text "message", null: false t.text "message", null: false
t.datetime "starts_at" t.datetime "starts_at"
...@@ -638,7 +650,6 @@ ActiveRecord::Schema.define(version: 20160530150109) do ...@@ -638,7 +650,6 @@ ActiveRecord::Schema.define(version: 20160530150109) do
t.boolean "system", default: false, null: false t.boolean "system", default: false, null: false
t.text "st_diff" t.text "st_diff"
t.integer "updated_by_id" t.integer "updated_by_id"
t.boolean "is_award", default: false, null: false
t.string "type" t.string "type"
end end
...@@ -646,7 +657,6 @@ ActiveRecord::Schema.define(version: 20160530150109) do ...@@ -646,7 +657,6 @@ ActiveRecord::Schema.define(version: 20160530150109) do
add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree
add_index "notes", ["created_at", "id"], name: "index_notes_on_created_at_and_id", using: :btree add_index "notes", ["created_at", "id"], name: "index_notes_on_created_at_and_id", using: :btree
add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
add_index "notes", ["is_award"], name: "index_notes_on_is_award", using: :btree
add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree
add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"} add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"}
add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
......
...@@ -278,6 +278,30 @@ Response: ...@@ -278,6 +278,30 @@ Response:
[ce-2893]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2893 [ce-2893]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2893
## Get a trace file
Get a trace of a specific build of a project
```
GET /projects/:id/builds/:build_id/trace
```
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
| id | integer | yes | The ID of a project |
| build_id | integer | yes | The ID of a build |
```
curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/trace"
```
Response:
| Status | Description |
|-----------|-----------------------------------|
| 200 | Serves the trace file |
| 404 | Build not found or no trace file |
## Cancel a build ## Cancel a build
Cancel a single build of a project Cancel a single build of a project
......
...@@ -29,7 +29,7 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps ...@@ -29,7 +29,7 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps
end end
step 'I click link "bug"' do step 'I click link "bug"' do
page.find('.js-label-select').click page.find('.js-label-select', visible: true).click
sleep 0.5 sleep 0.5
execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()") execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
end end
......
...@@ -191,15 +191,15 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps ...@@ -191,15 +191,15 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end end
step 'issue "Release 0.4" have 2 upvotes and 1 downvote' do step 'issue "Release 0.4" have 2 upvotes and 1 downvote' do
issue = Issue.find_by(title: 'Release 0.4') awardable = Issue.find_by(title: 'Release 0.4')
create_list(:upvote_note, 2, project: project, noteable: issue) create_list(:award_emoji, 2, awardable: awardable)
create(:downvote_note, project: project, noteable: issue) create(:award_emoji, :downvote, awardable: awardable)
end end
step 'issue "Tweet control" have 1 upvote and 2 downvotes' do step 'issue "Tweet control" have 1 upvote and 2 downvotes' do
issue = Issue.find_by(title: 'Tweet control') awardable = Issue.find_by(title: 'Tweet control')
create(:upvote_note, project: project, noteable: issue) create(:award_emoji, :upvote, awardable: awardable)
create_list(:downvote_note, 2, project: project, noteable: issue) create_list(:award_emoji, 2, awardable: awardable, name: 'thumbsdown')
end end
step 'The list should be sorted by "Least popular"' do step 'The list should be sorted by "Least popular"' do
......
...@@ -179,14 +179,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -179,14 +179,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'merge request "Bug NS-04" have 2 upvotes and 1 downvote' do step 'merge request "Bug NS-04" have 2 upvotes and 1 downvote' do
merge_request = MergeRequest.find_by(title: 'Bug NS-04') merge_request = MergeRequest.find_by(title: 'Bug NS-04')
create_list(:upvote_note, 2, project: project, noteable: merge_request) create_list(:award_emoji, 2, awardable: merge_request)
create(:downvote_note, project: project, noteable: merge_request) create(:award_emoji, :downvote, awardable: merge_request)
end end
step 'merge request "Bug NS-06" have 1 upvote and 2 downvotes' do step 'merge request "Bug NS-06" have 1 upvote and 2 downvotes' do
merge_request = MergeRequest.find_by(title: 'Bug NS-06') awardable = MergeRequest.find_by(title: 'Bug NS-06')
create(:upvote_note, project: project, noteable: merge_request) create(:award_emoji, awardable: awardable)
create_list(:downvote_note, 2, project: project, noteable: merge_request) create_list(:award_emoji, 2, :downvote, awardable: awardable)
end end
step 'The list should be sorted by "Least popular"' do step 'The list should be sorted by "Least popular"' do
......
...@@ -171,15 +171,17 @@ module API ...@@ -171,15 +171,17 @@ module API
expose :label_names, as: :labels expose :label_names, as: :labels
expose :milestone, using: Entities::Milestone expose :milestone, using: Entities::Milestone
expose :assignee, :author, using: Entities::UserBasic expose :assignee, :author, using: Entities::UserBasic
expose :subscribed do |issue, options| expose :subscribed do |issue, options|
issue.subscribed?(options[:current_user]) issue.subscribed?(options[:current_user])
end end
expose :user_notes_count expose :user_notes_count
expose :upvotes, :downvotes
end end
class MergeRequest < ProjectEntity class MergeRequest < ProjectEntity
expose :target_branch, :source_branch expose :target_branch, :source_branch
expose :upvotes, :downvotes expose :upvotes, :downvotes
expose :author, :assignee, using: Entities::UserBasic expose :author, :assignee, using: Entities::UserBasic
expose :source_project_id, :target_project_id expose :source_project_id, :target_project_id
expose :label_names, as: :labels expose :label_names, as: :labels
...@@ -217,8 +219,8 @@ module API ...@@ -217,8 +219,8 @@ module API
expose :system?, as: :system expose :system?, as: :system
expose :noteable_id, :noteable_type expose :noteable_id, :noteable_type
# upvote? and downvote? are deprecated, always return false # upvote? and downvote? are deprecated, always return false
expose :upvote?, as: :upvote expose(:upvote?) { |note| false }
expose :downvote?, as: :downvote expose(:downvote?) { |note| false }
end end
class MRNote < Grape::Entity class MRNote < Grape::Entity
......
...@@ -57,7 +57,7 @@ module API ...@@ -57,7 +57,7 @@ module API
not_found! "File" unless blob not_found! "File" unless blob
content_type 'text/plain' content_type 'text/plain'
header *Gitlab::Workhorse.send_git_blob(repo, blob) header(*Gitlab::Workhorse.send_git_blob(repo, blob))
end end
# Get a raw blob contents by blob sha # Get a raw blob contents by blob sha
...@@ -83,7 +83,7 @@ module API ...@@ -83,7 +83,7 @@ module API
env['api.format'] = :txt env['api.format'] = :txt
content_type blob.mime_type content_type blob.mime_type
header *Gitlab::Workhorse.send_git_blob(repo, blob) header(*Gitlab::Workhorse.send_git_blob(repo, blob))
end end
# Get a an archive of the repository # Get a an archive of the repository
...@@ -98,7 +98,7 @@ module API ...@@ -98,7 +98,7 @@ module API
authorize! :download_code, user_project authorize! :download_code, user_project
begin begin
header *Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format]) header(*Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format]))
rescue rescue
not_found!('File') not_found!('File')
end end
......
class AwardEmoji
CATEGORIES = {
other: "Other",
objects: "Objects",
places: "Places",
travel_places: "Travel",
emoticons: "Emoticons",
objects_symbols: "Symbols",
nature: "Nature",
celebration: "Celebration",
people: "People",
activity: "Activity",
flags: "Flags",
food_drink: "Food"
}.with_indifferent_access
CATEGORY_ALIASES = {
symbols: "objects_symbols",
foods: "food_drink",
travel: "travel_places"
}.with_indifferent_access
def self.normilize_emoji_name(name)
aliases[name] || name
end
def self.emoji_by_category
unless @emoji_by_category
@emoji_by_category = Hash.new { |h, key| h[key] = [] }
emojis.each do |emoji_name, data|
data["name"] = emoji_name
# Skip Fitzpatrick(tone) modifiers
next if data["category"] == "modifier"
category = CATEGORY_ALIASES[data["category"]] || data["category"]
@emoji_by_category[category] << data
end
@emoji_by_category = @emoji_by_category.sort.to_h
end
@emoji_by_category
end
def self.emojis
@emojis ||= begin
json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' )
JSON.parse(File.read(json_path))
end
end
def self.unicode
@unicode ||= emojis.map {|key, value| { key => emojis[key]["unicode"] } }.inject(:merge!)
end
def self.aliases
@aliases ||= begin
json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' )
JSON.parse(File.read(json_path))
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
...@@ -86,9 +86,9 @@ module Backup ...@@ -86,9 +86,9 @@ module Backup
def report_success(success) def report_success(success)
if success if success
$progress.puts '[DONE]'.green $progress.puts '[DONE]'.color(:green)
else else
$progress.puts '[FAILED]'.red $progress.puts '[FAILED]'.color(:red)
end end
end end
end end
......
...@@ -27,9 +27,9 @@ module Backup ...@@ -27,9 +27,9 @@ module Backup
# Set file permissions on open to prevent chmod races. # Set file permissions on open to prevent chmod races.
tar_system_options = {out: [tar_file, 'w', Gitlab.config.backup.archive_permissions]} tar_system_options = {out: [tar_file, 'w', Gitlab.config.backup.archive_permissions]}
if Kernel.system('tar', '-cf', '-', *backup_contents, tar_system_options) if Kernel.system('tar', '-cf', '-', *backup_contents, tar_system_options)
$progress.puts "done".green $progress.puts "done".color(:green)
else else
puts "creating archive #{tar_file} failed".red puts "creating archive #{tar_file} failed".color(:red)
abort 'Backup failed' abort 'Backup failed'
end end
...@@ -43,7 +43,7 @@ module Backup ...@@ -43,7 +43,7 @@ module Backup
connection_settings = Gitlab.config.backup.upload.connection connection_settings = Gitlab.config.backup.upload.connection
if connection_settings.blank? if connection_settings.blank?
$progress.puts "skipped".yellow $progress.puts "skipped".color(:yellow)
return return
end end
...@@ -53,9 +53,9 @@ module Backup ...@@ -53,9 +53,9 @@ module Backup
if directory.files.create(key: tar_file, body: File.open(tar_file), public: false, if directory.files.create(key: tar_file, body: File.open(tar_file), public: false,
multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size, multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
encryption: Gitlab.config.backup.upload.encryption) encryption: Gitlab.config.backup.upload.encryption)
$progress.puts "done".green $progress.puts "done".color(:green)
else else
puts "uploading backup to #{remote_directory} failed".red puts "uploading backup to #{remote_directory} failed".color(:red)
abort 'Backup failed' abort 'Backup failed'
end end
end end
...@@ -67,9 +67,9 @@ module Backup ...@@ -67,9 +67,9 @@ module Backup
next unless File.exist?(File.join(Gitlab.config.backup.path, dir)) next unless File.exist?(File.join(Gitlab.config.backup.path, dir))
if FileUtils.rm_rf(File.join(Gitlab.config.backup.path, dir)) if FileUtils.rm_rf(File.join(Gitlab.config.backup.path, dir))
$progress.puts "done".green $progress.puts "done".color(:green)
else else
puts "deleting tmp directory '#{dir}' failed".red puts "deleting tmp directory '#{dir}' failed".color(:red)
abort 'Backup failed' abort 'Backup failed'
end end
end end
...@@ -95,9 +95,9 @@ module Backup ...@@ -95,9 +95,9 @@ module Backup
end end
end end
$progress.puts "done. (#{removed} removed)".green $progress.puts "done. (#{removed} removed)".color(:green)
else else
$progress.puts "skipping".yellow $progress.puts "skipping".color(:yellow)
end end
end end
...@@ -124,20 +124,20 @@ module Backup ...@@ -124,20 +124,20 @@ module Backup
$progress.print "Unpacking backup ... " $progress.print "Unpacking backup ... "
unless Kernel.system(*%W(tar -xf #{tar_file})) unless Kernel.system(*%W(tar -xf #{tar_file}))
puts "unpacking backup failed".red puts "unpacking backup failed".color(:red)
exit 1 exit 1
else else
$progress.puts "done".green $progress.puts "done".color(:green)
end end
ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0 ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0
# restoring mismatching backups can lead to unexpected problems # restoring mismatching backups can lead to unexpected problems
if settings[:gitlab_version] != Gitlab::VERSION if settings[:gitlab_version] != Gitlab::VERSION
puts "GitLab version mismatch:".red puts "GitLab version mismatch:".color(:red)
puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".red puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".color(:red)
puts " Please switch to the following version and try again:".red puts " Please switch to the following version and try again:".color(:red)
puts " version: #{settings[:gitlab_version]}".red puts " version: #{settings[:gitlab_version]}".color(:red)
puts puts
puts "Hint: git checkout v#{settings[:gitlab_version]}" puts "Hint: git checkout v#{settings[:gitlab_version]}"
exit 1 exit 1
......
...@@ -14,14 +14,14 @@ module Backup ...@@ -14,14 +14,14 @@ module Backup
FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.path)) if project.namespace FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.path)) if project.namespace
if project.empty_repo? if project.empty_repo?
$progress.puts "[SKIPPED]".cyan $progress.puts "[SKIPPED]".color(:cyan)
else else
cmd = %W(tar -cf #{path_to_bundle(project)} -C #{path_to_repo(project)} .) cmd = %W(tar -cf #{path_to_bundle(project)} -C #{path_to_repo(project)} .)
output, status = Gitlab::Popen.popen(cmd) output, status = Gitlab::Popen.popen(cmd)
if status.zero? if status.zero?
$progress.puts "[DONE]".green $progress.puts "[DONE]".color(:green)
else else
puts "[FAILED]".red puts "[FAILED]".color(:red)
puts "failed: #{cmd.join(' ')}" puts "failed: #{cmd.join(' ')}"
puts output puts output
abort 'Backup failed' abort 'Backup failed'
...@@ -33,14 +33,14 @@ module Backup ...@@ -33,14 +33,14 @@ module Backup
if File.exists?(path_to_repo(wiki)) if File.exists?(path_to_repo(wiki))
$progress.print " * #{wiki.path_with_namespace} ... " $progress.print " * #{wiki.path_with_namespace} ... "
if wiki.repository.empty? if wiki.repository.empty?
$progress.puts " [SKIPPED]".cyan $progress.puts " [SKIPPED]".color(:cyan)
else else
cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all) cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all)
output, status = Gitlab::Popen.popen(cmd) output, status = Gitlab::Popen.popen(cmd)
if status.zero? if status.zero?
$progress.puts " [DONE]".green $progress.puts " [DONE]".color(:green)
else else
puts " [FAILED]".red puts " [FAILED]".color(:red)
puts "failed: #{cmd.join(' ')}" puts "failed: #{cmd.join(' ')}"
abort 'Backup failed' abort 'Backup failed'
end end
...@@ -71,9 +71,9 @@ module Backup ...@@ -71,9 +71,9 @@ module Backup
end end
if system(*cmd, silent) if system(*cmd, silent)
$progress.puts "[DONE]".green $progress.puts "[DONE]".color(:green)
else else
puts "[FAILED]".red puts "[FAILED]".color(:red)
puts "failed: #{cmd.join(' ')}" puts "failed: #{cmd.join(' ')}"
abort 'Restore failed' abort 'Restore failed'
end end
...@@ -90,21 +90,21 @@ module Backup ...@@ -90,21 +90,21 @@ module Backup
cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)}) cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)})
if system(*cmd, silent) if system(*cmd, silent)
$progress.puts " [DONE]".green $progress.puts " [DONE]".color(:green)
else else
puts " [FAILED]".red puts " [FAILED]".color(:red)
puts "failed: #{cmd.join(' ')}" puts "failed: #{cmd.join(' ')}"
abort 'Restore failed' abort 'Restore failed'
end end
end end
end end
$progress.print 'Put GitLab hooks in repositories dirs'.yellow $progress.print 'Put GitLab hooks in repositories dirs'.color(:yellow)
cmd = "#{Gitlab.config.gitlab_shell.path}/bin/create-hooks" cmd = "#{Gitlab.config.gitlab_shell.path}/bin/create-hooks"
if system(cmd) if system(cmd)
$progress.puts " [DONE]".green $progress.puts " [DONE]".color(:green)
else else
puts " [FAILED]".red puts " [FAILED]".color(:red)
puts "failed: #{cmd}" puts "failed: #{cmd}"
end end
......
module Gitlab
class AwardEmoji
CATEGORIES = {
other: "Other",
objects: "Objects",
places: "Places",
travel_places: "Travel",
emoticons: "Emoticons",
objects_symbols: "Symbols",
nature: "Nature",
celebration: "Celebration",
people: "People",
activity: "Activity",
flags: "Flags",
food_drink: "Food"
}.with_indifferent_access
CATEGORY_ALIASES = {
symbols: "objects_symbols",
foods: "food_drink",
travel: "travel_places"
}.with_indifferent_access
def self.normalize_emoji_name(name)
aliases[name] || name
end
def self.emoji_by_category
unless @emoji_by_category
@emoji_by_category = Hash.new { |h, key| h[key] = [] }
emojis.each do |emoji_name, data|
data["name"] = emoji_name
# Skip Fitzpatrick(tone) modifiers
next if data["category"] == "modifier"
category = CATEGORY_ALIASES[data["category"]] || data["category"]
@emoji_by_category[category] << data
end
@emoji_by_category = @emoji_by_category.sort.to_h
end
@emoji_by_category
end
def self.emojis
@emojis ||=
begin
json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' )
JSON.parse(File.read(json_path))
end
end
def self.aliases
@aliases ||=
begin
json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' )
JSON.parse(File.read(json_path))
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
...@@ -17,9 +17,9 @@ module Gitlab ...@@ -17,9 +17,9 @@ module Gitlab
file.rewind file.rewind
cmd = [] cmd = []
cmd.push *%W(ssh-keygen) cmd.push('ssh-keygen')
cmd.push *%W(-E md5) if explicit_fingerprint_algorithm? cmd.push('-E', 'md5') if explicit_fingerprint_algorithm?
cmd.push *%W(-lf #{file.path}) cmd.push('-lf', file.path)
cmd_output, cmd_status = popen(cmd, '/tmp') cmd_output, cmd_status = popen(cmd, '/tmp')
end end
......
...@@ -93,6 +93,7 @@ module Gitlab ...@@ -93,6 +93,7 @@ module Gitlab
end end
protected protected
def base_config def base_config
Gitlab.config.ldap Gitlab.config.ldap
end end
......
...@@ -5,7 +5,7 @@ module Gitlab ...@@ -5,7 +5,7 @@ module Gitlab
SeedFu.quiet = true SeedFu.quiet = true
yield yield
SeedFu.quiet = false SeedFu.quiet = false
puts "\nOK".green puts "\nOK".color(:green)
end end
def self.by_user(user) def self.by_user(user)
......
...@@ -40,14 +40,14 @@ namespace :gitlab do ...@@ -40,14 +40,14 @@ namespace :gitlab do
removed. removed.
MSG MSG
ask_to_continue ask_to_continue
puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.yellow puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.color(:yellow)
sleep(5) sleep(5)
end end
# Drop all tables Load the schema to ensure we don't have any newer tables # Drop all tables Load the schema to ensure we don't have any newer tables
# hanging out from a failed upgrade # hanging out from a failed upgrade
$progress.puts 'Cleaning the database ... '.blue $progress.puts 'Cleaning the database ... '.color(:blue)
Rake::Task['gitlab:db:drop_tables'].invoke Rake::Task['gitlab:db:drop_tables'].invoke
$progress.puts 'done'.green $progress.puts 'done'.color(:green)
Rake::Task['gitlab:backup:db:restore'].invoke Rake::Task['gitlab:backup:db:restore'].invoke
end end
Rake::Task['gitlab:backup:repo:restore'].invoke unless backup.skipped?('repositories') Rake::Task['gitlab:backup:repo:restore'].invoke unless backup.skipped?('repositories')
...@@ -63,141 +63,141 @@ namespace :gitlab do ...@@ -63,141 +63,141 @@ namespace :gitlab do
namespace :repo do namespace :repo do
task create: :environment do task create: :environment do
$progress.puts "Dumping repositories ...".blue $progress.puts "Dumping repositories ...".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("repositories") if ENV["SKIP"] && ENV["SKIP"].include?("repositories")
$progress.puts "[SKIPPED]".cyan $progress.puts "[SKIPPED]".color(:cyan)
else else
Backup::Repository.new.dump Backup::Repository.new.dump
$progress.puts "done".green $progress.puts "done".color(:green)
end end
end end
task restore: :environment do task restore: :environment do
$progress.puts "Restoring repositories ...".blue $progress.puts "Restoring repositories ...".color(:blue)
Backup::Repository.new.restore Backup::Repository.new.restore
$progress.puts "done".green $progress.puts "done".color(:green)
end end
end end
namespace :db do namespace :db do
task create: :environment do task create: :environment do
$progress.puts "Dumping database ... ".blue $progress.puts "Dumping database ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("db") if ENV["SKIP"] && ENV["SKIP"].include?("db")
$progress.puts "[SKIPPED]".cyan $progress.puts "[SKIPPED]".color(:cyan)
else else
Backup::Database.new.dump Backup::Database.new.dump
$progress.puts "done".green $progress.puts "done".color(:green)
end end
end end
task restore: :environment do task restore: :environment do
$progress.puts "Restoring database ... ".blue $progress.puts "Restoring database ... ".color(:blue)
Backup::Database.new.restore Backup::Database.new.restore
$progress.puts "done".green $progress.puts "done".color(:green)
end end
end end
namespace :builds do namespace :builds do
task create: :environment do task create: :environment do
$progress.puts "Dumping builds ... ".blue $progress.puts "Dumping builds ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("builds") if ENV["SKIP"] && ENV["SKIP"].include?("builds")
$progress.puts "[SKIPPED]".cyan $progress.puts "[SKIPPED]".color(:cyan)
else else
Backup::Builds.new.dump Backup::Builds.new.dump
$progress.puts "done".green $progress.puts "done".color(:green)
end end
end end
task restore: :environment do task restore: :environment do
$progress.puts "Restoring builds ... ".blue $progress.puts "Restoring builds ... ".color(:blue)
Backup::Builds.new.restore Backup::Builds.new.restore
$progress.puts "done".green $progress.puts "done".color(:green)
end end
end end
namespace :uploads do namespace :uploads do
task create: :environment do task create: :environment do
$progress.puts "Dumping uploads ... ".blue $progress.puts "Dumping uploads ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("uploads") if ENV["SKIP"] && ENV["SKIP"].include?("uploads")
$progress.puts "[SKIPPED]".cyan $progress.puts "[SKIPPED]".color(:cyan)
else else
Backup::Uploads.new.dump Backup::Uploads.new.dump
$progress.puts "done".green $progress.puts "done".color(:green)
end end
end end
task restore: :environment do task restore: :environment do
$progress.puts "Restoring uploads ... ".blue $progress.puts "Restoring uploads ... ".color(:blue)
Backup::Uploads.new.restore Backup::Uploads.new.restore
$progress.puts "done".green $progress.puts "done".color(:green)
end end
end end
namespace :artifacts do namespace :artifacts do
task create: :environment do task create: :environment do
$progress.puts "Dumping artifacts ... ".blue $progress.puts "Dumping artifacts ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("artifacts") if ENV["SKIP"] && ENV["SKIP"].include?("artifacts")
$progress.puts "[SKIPPED]".cyan $progress.puts "[SKIPPED]".color(:cyan)
else else
Backup::Artifacts.new.dump Backup::Artifacts.new.dump
$progress.puts "done".green $progress.puts "done".color(:green)
end end
end end
task restore: :environment do task restore: :environment do
$progress.puts "Restoring artifacts ... ".blue $progress.puts "Restoring artifacts ... ".color(:blue)
Backup::Artifacts.new.restore Backup::Artifacts.new.restore
$progress.puts "done".green $progress.puts "done".color(:green)
end end
end end
namespace :lfs do namespace :lfs do
task create: :environment do task create: :environment do
$progress.puts "Dumping lfs objects ... ".blue $progress.puts "Dumping lfs objects ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("lfs") if ENV["SKIP"] && ENV["SKIP"].include?("lfs")
$progress.puts "[SKIPPED]".cyan $progress.puts "[SKIPPED]".color(:cyan)
else else
Backup::Lfs.new.dump Backup::Lfs.new.dump
$progress.puts "done".green $progress.puts "done".color(:green)
end end
end end
task restore: :environment do task restore: :environment do
$progress.puts "Restoring lfs objects ... ".blue $progress.puts "Restoring lfs objects ... ".color(:blue)
Backup::Lfs.new.restore Backup::Lfs.new.restore
$progress.puts "done".green $progress.puts "done".color(:green)
end end
end end
namespace :registry do namespace :registry do
task create: :environment do task create: :environment do
$progress.puts "Dumping container registry images ... ".blue $progress.puts "Dumping container registry images ... ".color(:blue)
if Gitlab.config.registry.enabled if Gitlab.config.registry.enabled
if ENV["SKIP"] && ENV["SKIP"].include?("registry") if ENV["SKIP"] && ENV["SKIP"].include?("registry")
$progress.puts "[SKIPPED]".cyan $progress.puts "[SKIPPED]".color(:cyan)
else else
Backup::Registry.new.dump Backup::Registry.new.dump
$progress.puts "done".green $progress.puts "done".color(:green)
end end
else else
$progress.puts "[DISABLED]".cyan $progress.puts "[DISABLED]".color(:cyan)
end end
end end
task restore: :environment do task restore: :environment do
$progress.puts "Restoring container registry images ... ".blue $progress.puts "Restoring container registry images ... ".color(:blue)
if Gitlab.config.registry.enabled if Gitlab.config.registry.enabled
Backup::Registry.new.restore Backup::Registry.new.restore
$progress.puts "done".green $progress.puts "done".color(:green)
else else
$progress.puts "[DISABLED]".cyan $progress.puts "[DISABLED]".color(:cyan)
end end
end end
end end
......
...@@ -50,14 +50,14 @@ namespace :gitlab do ...@@ -50,14 +50,14 @@ namespace :gitlab do
end end
if correct_options.all? if correct_options.all?
puts "yes".green puts "yes".color(:green)
else else
print "Trying to fix Git error automatically. ..." print "Trying to fix Git error automatically. ..."
if auto_fix_git_config(options) if auto_fix_git_config(options)
puts "Success".green puts "Success".color(:green)
else else
puts "Failed".red puts "Failed".color(:red)
try_fixing_it( try_fixing_it(
sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"") sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"")
) )
...@@ -74,9 +74,9 @@ namespace :gitlab do ...@@ -74,9 +74,9 @@ namespace :gitlab do
database_config_file = Rails.root.join("config", "database.yml") database_config_file = Rails.root.join("config", "database.yml")
if File.exists?(database_config_file) if File.exists?(database_config_file)
puts "yes".green puts "yes".color(:green)
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
"Copy config/database.yml.<your db> to config/database.yml", "Copy config/database.yml.<your db> to config/database.yml",
"Check that the information in config/database.yml is correct" "Check that the information in config/database.yml is correct"
...@@ -95,9 +95,9 @@ namespace :gitlab do ...@@ -95,9 +95,9 @@ namespace :gitlab do
gitlab_config_file = Rails.root.join("config", "gitlab.yml") gitlab_config_file = Rails.root.join("config", "gitlab.yml")
if File.exists?(gitlab_config_file) if File.exists?(gitlab_config_file)
puts "yes".green puts "yes".color(:green)
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
"Copy config/gitlab.yml.example to config/gitlab.yml", "Copy config/gitlab.yml.example to config/gitlab.yml",
"Update config/gitlab.yml to match your setup" "Update config/gitlab.yml to match your setup"
...@@ -114,14 +114,14 @@ namespace :gitlab do ...@@ -114,14 +114,14 @@ namespace :gitlab do
gitlab_config_file = Rails.root.join("config", "gitlab.yml") gitlab_config_file = Rails.root.join("config", "gitlab.yml")
unless File.exists?(gitlab_config_file) unless File.exists?(gitlab_config_file)
puts "can't check because of previous errors".magenta puts "can't check because of previous errors".color(:magenta)
end end
# omniauth or ldap could have been deleted from the file # omniauth or ldap could have been deleted from the file
unless Gitlab.config['git_host'] unless Gitlab.config['git_host']
puts "no".green puts "no".color(:green)
else else
puts "yes".red puts "yes".color(:red)
try_fixing_it( try_fixing_it(
"Backup your config/gitlab.yml", "Backup your config/gitlab.yml",
"Copy config/gitlab.yml.example to config/gitlab.yml", "Copy config/gitlab.yml.example to config/gitlab.yml",
...@@ -138,16 +138,16 @@ namespace :gitlab do ...@@ -138,16 +138,16 @@ namespace :gitlab do
print "Init script exists? ... " print "Init script exists? ... "
if omnibus_gitlab? if omnibus_gitlab?
puts 'skipped (omnibus-gitlab has no init script)'.magenta puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta)
return return
end end
script_path = "/etc/init.d/gitlab" script_path = "/etc/init.d/gitlab"
if File.exists?(script_path) if File.exists?(script_path)
puts "yes".green puts "yes".color(:green)
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
"Install the init script" "Install the init script"
) )
...@@ -162,7 +162,7 @@ namespace :gitlab do ...@@ -162,7 +162,7 @@ namespace :gitlab do
print "Init script up-to-date? ... " print "Init script up-to-date? ... "
if omnibus_gitlab? if omnibus_gitlab?
puts 'skipped (omnibus-gitlab has no init script)'.magenta puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta)
return return
end end
...@@ -170,7 +170,7 @@ namespace :gitlab do ...@@ -170,7 +170,7 @@ namespace :gitlab do
script_path = "/etc/init.d/gitlab" script_path = "/etc/init.d/gitlab"
unless File.exists?(script_path) unless File.exists?(script_path)
puts "can't check because of previous errors".magenta puts "can't check because of previous errors".color(:magenta)
return return
end end
...@@ -178,9 +178,9 @@ namespace :gitlab do ...@@ -178,9 +178,9 @@ namespace :gitlab do
script_content = File.read(script_path) script_content = File.read(script_path)
if recipe_content == script_content if recipe_content == script_content
puts "yes".green puts "yes".color(:green)
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
"Redownload the init script" "Redownload the init script"
) )
...@@ -197,9 +197,9 @@ namespace :gitlab do ...@@ -197,9 +197,9 @@ namespace :gitlab do
migration_status, _ = Gitlab::Popen.popen(%W(bundle exec rake db:migrate:status)) migration_status, _ = Gitlab::Popen.popen(%W(bundle exec rake db:migrate:status))
unless migration_status =~ /down\s+\d{14}/ unless migration_status =~ /down\s+\d{14}/
puts "yes".green puts "yes".color(:green)
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
sudo_gitlab("bundle exec rake db:migrate RAILS_ENV=production") sudo_gitlab("bundle exec rake db:migrate RAILS_ENV=production")
) )
...@@ -210,13 +210,13 @@ namespace :gitlab do ...@@ -210,13 +210,13 @@ namespace :gitlab do
def check_orphaned_group_members def check_orphaned_group_members
print "Database contains orphaned GroupMembers? ... " print "Database contains orphaned GroupMembers? ... "
if GroupMember.where("user_id not in (select id from users)").count > 0 if GroupMember.where("user_id not in (select id from users)").count > 0
puts "yes".red puts "yes".color(:red)
try_fixing_it( try_fixing_it(
"You can delete the orphaned records using something along the lines of:", "You can delete the orphaned records using something along the lines of:",
sudo_gitlab("bundle exec rails runner -e production 'GroupMember.where(\"user_id NOT IN (SELECT id FROM users)\").delete_all'") sudo_gitlab("bundle exec rails runner -e production 'GroupMember.where(\"user_id NOT IN (SELECT id FROM users)\").delete_all'")
) )
else else
puts "no".green puts "no".color(:green)
end end
end end
...@@ -226,9 +226,9 @@ namespace :gitlab do ...@@ -226,9 +226,9 @@ namespace :gitlab do
log_path = Rails.root.join("log") log_path = Rails.root.join("log")
if File.writable?(log_path) if File.writable?(log_path)
puts "yes".green puts "yes".color(:green)
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
"sudo chown -R gitlab #{log_path}", "sudo chown -R gitlab #{log_path}",
"sudo chmod -R u+rwX #{log_path}" "sudo chmod -R u+rwX #{log_path}"
...@@ -246,9 +246,9 @@ namespace :gitlab do ...@@ -246,9 +246,9 @@ namespace :gitlab do
tmp_path = Rails.root.join("tmp") tmp_path = Rails.root.join("tmp")
if File.writable?(tmp_path) if File.writable?(tmp_path)
puts "yes".green puts "yes".color(:green)
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
"sudo chown -R gitlab #{tmp_path}", "sudo chown -R gitlab #{tmp_path}",
"sudo chmod -R u+rwX #{tmp_path}" "sudo chmod -R u+rwX #{tmp_path}"
...@@ -264,7 +264,7 @@ namespace :gitlab do ...@@ -264,7 +264,7 @@ namespace :gitlab do
print "Uploads directory setup correctly? ... " print "Uploads directory setup correctly? ... "
unless File.directory?(Rails.root.join('public/uploads')) unless File.directory?(Rails.root.join('public/uploads'))
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
"sudo -u #{gitlab_user} mkdir #{Rails.root}/public/uploads" "sudo -u #{gitlab_user} mkdir #{Rails.root}/public/uploads"
) )
...@@ -280,16 +280,16 @@ namespace :gitlab do ...@@ -280,16 +280,16 @@ namespace :gitlab do
if File.stat(upload_path).mode == 040700 if File.stat(upload_path).mode == 040700
unless Dir.exists?(upload_path_tmp) unless Dir.exists?(upload_path_tmp)
puts 'skipped (no tmp uploads folder yet)'.magenta puts 'skipped (no tmp uploads folder yet)'.color(:magenta)
return return
end end
# If tmp upload dir has incorrect permissions, assume others do as well # If tmp upload dir has incorrect permissions, assume others do as well
# Verify drwx------ permissions # Verify drwx------ permissions
if File.stat(upload_path_tmp).mode == 040700 && File.owned?(upload_path_tmp) if File.stat(upload_path_tmp).mode == 040700 && File.owned?(upload_path_tmp)
puts "yes".green puts "yes".color(:green)
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
"sudo chown -R #{gitlab_user} #{upload_path}", "sudo chown -R #{gitlab_user} #{upload_path}",
"sudo find #{upload_path} -type f -exec chmod 0644 {} \\;", "sudo find #{upload_path} -type f -exec chmod 0644 {} \\;",
...@@ -301,7 +301,7 @@ namespace :gitlab do ...@@ -301,7 +301,7 @@ namespace :gitlab do
fix_and_rerun fix_and_rerun
end end
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
"sudo chmod 700 #{upload_path}" "sudo chmod 700 #{upload_path}"
) )
...@@ -320,9 +320,9 @@ namespace :gitlab do ...@@ -320,9 +320,9 @@ namespace :gitlab do
redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/) redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/)
if redis_version && if redis_version &&
(Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version)) (Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version))
puts "yes".green puts "yes".color(:green)
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
"Update your redis server to a version >= #{min_redis_version}" "Update your redis server to a version >= #{min_redis_version}"
) )
...@@ -361,10 +361,10 @@ namespace :gitlab do ...@@ -361,10 +361,10 @@ namespace :gitlab do
repo_base_path = Gitlab.config.gitlab_shell.repos_path repo_base_path = Gitlab.config.gitlab_shell.repos_path
if File.exists?(repo_base_path) if File.exists?(repo_base_path)
puts "yes".green puts "yes".color(:green)
else else
puts "no".red puts "no".color(:red)
puts "#{repo_base_path} is missing".red puts "#{repo_base_path} is missing".color(:red)
try_fixing_it( try_fixing_it(
"This should have been created when setting up GitLab Shell.", "This should have been created when setting up GitLab Shell.",
"Make sure it's set correctly in config/gitlab.yml", "Make sure it's set correctly in config/gitlab.yml",
...@@ -382,14 +382,14 @@ namespace :gitlab do ...@@ -382,14 +382,14 @@ namespace :gitlab do
repo_base_path = Gitlab.config.gitlab_shell.repos_path repo_base_path = Gitlab.config.gitlab_shell.repos_path
unless File.exists?(repo_base_path) unless File.exists?(repo_base_path)
puts "can't check because of previous errors".magenta puts "can't check because of previous errors".color(:magenta)
return return
end end
unless File.symlink?(repo_base_path) unless File.symlink?(repo_base_path)
puts "no".green puts "no".color(:green)
else else
puts "yes".red puts "yes".color(:red)
try_fixing_it( try_fixing_it(
"Make sure it's set to the real directory in config/gitlab.yml" "Make sure it's set to the real directory in config/gitlab.yml"
) )
...@@ -402,14 +402,14 @@ namespace :gitlab do ...@@ -402,14 +402,14 @@ namespace :gitlab do
repo_base_path = Gitlab.config.gitlab_shell.repos_path repo_base_path = Gitlab.config.gitlab_shell.repos_path
unless File.exists?(repo_base_path) unless File.exists?(repo_base_path)
puts "can't check because of previous errors".magenta puts "can't check because of previous errors".color(:magenta)
return return
end end
if File.stat(repo_base_path).mode.to_s(8).ends_with?("2770") if File.stat(repo_base_path).mode.to_s(8).ends_with?("2770")
puts "yes".green puts "yes".color(:green)
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
"sudo chmod -R ug+rwX,o-rwx #{repo_base_path}", "sudo chmod -R ug+rwX,o-rwx #{repo_base_path}",
"sudo chmod -R ug-s #{repo_base_path}", "sudo chmod -R ug-s #{repo_base_path}",
...@@ -429,17 +429,17 @@ namespace :gitlab do ...@@ -429,17 +429,17 @@ namespace :gitlab do
repo_base_path = Gitlab.config.gitlab_shell.repos_path repo_base_path = Gitlab.config.gitlab_shell.repos_path
unless File.exists?(repo_base_path) unless File.exists?(repo_base_path)
puts "can't check because of previous errors".magenta puts "can't check because of previous errors".color(:magenta)
return return
end end
uid = uid_for(gitlab_shell_ssh_user) uid = uid_for(gitlab_shell_ssh_user)
gid = gid_for(gitlab_shell_owner_group) gid = gid_for(gitlab_shell_owner_group)
if File.stat(repo_base_path).uid == uid && File.stat(repo_base_path).gid == gid if File.stat(repo_base_path).uid == uid && File.stat(repo_base_path).gid == gid
puts "yes".green puts "yes".color(:green)
else else
puts "no".red puts "no".color(:red)
puts " User id for #{gitlab_shell_ssh_user}: #{uid}. Groupd id for #{gitlab_shell_owner_group}: #{gid}".blue puts " User id for #{gitlab_shell_ssh_user}: #{uid}. Groupd id for #{gitlab_shell_owner_group}: #{gid}".color(:blue)
try_fixing_it( try_fixing_it(
"sudo chown -R #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group} #{repo_base_path}" "sudo chown -R #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group} #{repo_base_path}"
) )
...@@ -456,7 +456,7 @@ namespace :gitlab do ...@@ -456,7 +456,7 @@ namespace :gitlab do
gitlab_shell_hooks_path = Gitlab.config.gitlab_shell.hooks_path gitlab_shell_hooks_path = Gitlab.config.gitlab_shell.hooks_path
unless Project.count > 0 unless Project.count > 0
puts "can't check, you have no projects".magenta puts "can't check, you have no projects".color(:magenta)
return return
end end
puts "" puts ""
...@@ -466,12 +466,12 @@ namespace :gitlab do ...@@ -466,12 +466,12 @@ namespace :gitlab do
project_hook_directory = File.join(project.repository.path_to_repo, "hooks") project_hook_directory = File.join(project.repository.path_to_repo, "hooks")
if project.empty_repo? if project.empty_repo?
puts "repository is empty".magenta puts "repository is empty".color(:magenta)
elsif File.directory?(project_hook_directory) && File.directory?(gitlab_shell_hooks_path) && elsif File.directory?(project_hook_directory) && File.directory?(gitlab_shell_hooks_path) &&
(File.realpath(project_hook_directory) == File.realpath(gitlab_shell_hooks_path)) (File.realpath(project_hook_directory) == File.realpath(gitlab_shell_hooks_path))
puts 'ok'.green puts 'ok'.color(:green)
else else
puts "wrong or missing hooks".red puts "wrong or missing hooks".color(:red)
try_fixing_it( try_fixing_it(
sudo_gitlab("#{File.join(gitlab_shell_path, 'bin/create-hooks')}"), sudo_gitlab("#{File.join(gitlab_shell_path, 'bin/create-hooks')}"),
'Check the hooks_path in config/gitlab.yml', 'Check the hooks_path in config/gitlab.yml',
...@@ -491,9 +491,9 @@ namespace :gitlab do ...@@ -491,9 +491,9 @@ namespace :gitlab do
check_cmd = File.expand_path('bin/check', gitlab_shell_repo_base) check_cmd = File.expand_path('bin/check', gitlab_shell_repo_base)
puts "Running #{check_cmd}" puts "Running #{check_cmd}"
if system(check_cmd, chdir: gitlab_shell_repo_base) if system(check_cmd, chdir: gitlab_shell_repo_base)
puts 'gitlab-shell self-check successful'.green puts 'gitlab-shell self-check successful'.color(:green)
else else
puts 'gitlab-shell self-check failed'.red puts 'gitlab-shell self-check failed'.color(:red)
try_fixing_it( try_fixing_it(
'Make sure GitLab is running;', 'Make sure GitLab is running;',
'Check the gitlab-shell configuration file:', 'Check the gitlab-shell configuration file:',
...@@ -507,7 +507,7 @@ namespace :gitlab do ...@@ -507,7 +507,7 @@ namespace :gitlab do
print "projects have namespace: ... " print "projects have namespace: ... "
unless Project.count > 0 unless Project.count > 0
puts "can't check, you have no projects".magenta puts "can't check, you have no projects".color(:magenta)
return return
end end
puts "" puts ""
...@@ -516,9 +516,9 @@ namespace :gitlab do ...@@ -516,9 +516,9 @@ namespace :gitlab do
print sanitized_message(project) print sanitized_message(project)
if project.namespace if project.namespace
puts "yes".green puts "yes".color(:green)
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
"Migrate global projects" "Migrate global projects"
) )
...@@ -576,9 +576,9 @@ namespace :gitlab do ...@@ -576,9 +576,9 @@ namespace :gitlab do
print "Running? ... " print "Running? ... "
if sidekiq_process_count > 0 if sidekiq_process_count > 0
puts "yes".green puts "yes".color(:green)
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
sudo_gitlab("RAILS_ENV=production bin/background_jobs start") sudo_gitlab("RAILS_ENV=production bin/background_jobs start")
) )
...@@ -596,9 +596,9 @@ namespace :gitlab do ...@@ -596,9 +596,9 @@ namespace :gitlab do
print 'Number of Sidekiq processes ... ' print 'Number of Sidekiq processes ... '
if process_count == 1 if process_count == 1
puts '1'.green puts '1'.color(:green)
else else
puts "#{process_count}".red puts "#{process_count}".color(:red)
try_fixing_it( try_fixing_it(
'sudo service gitlab stop', 'sudo service gitlab stop',
"sudo pkill -u #{gitlab_user} -f sidekiq", "sudo pkill -u #{gitlab_user} -f sidekiq",
...@@ -646,16 +646,16 @@ namespace :gitlab do ...@@ -646,16 +646,16 @@ namespace :gitlab do
print "Init.d configured correctly? ... " print "Init.d configured correctly? ... "
if omnibus_gitlab? if omnibus_gitlab?
puts 'skipped (omnibus-gitlab has no init script)'.magenta puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta)
return return
end end
path = "/etc/default/gitlab" path = "/etc/default/gitlab"
if File.exist?(path) && File.read(path).include?("mail_room_enabled=true") if File.exist?(path) && File.read(path).include?("mail_room_enabled=true")
puts "yes".green puts "yes".color(:green)
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
"Enable mail_room in the init.d configuration." "Enable mail_room in the init.d configuration."
) )
...@@ -672,9 +672,9 @@ namespace :gitlab do ...@@ -672,9 +672,9 @@ namespace :gitlab do
path = Rails.root.join("Procfile") path = Rails.root.join("Procfile")
if File.exist?(path) && File.read(path) =~ /^mail_room:/ if File.exist?(path) && File.read(path) =~ /^mail_room:/
puts "yes".green puts "yes".color(:green)
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
"Enable mail_room in your Procfile." "Enable mail_room in your Procfile."
) )
...@@ -691,14 +691,14 @@ namespace :gitlab do ...@@ -691,14 +691,14 @@ namespace :gitlab do
path = "/etc/default/gitlab" path = "/etc/default/gitlab"
unless File.exist?(path) && File.read(path).include?("mail_room_enabled=true") unless File.exist?(path) && File.read(path).include?("mail_room_enabled=true")
puts "can't check because of previous errors".magenta puts "can't check because of previous errors".color(:magenta)
return return
end end
if mail_room_running? if mail_room_running?
puts "yes".green puts "yes".color(:green)
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
sudo_gitlab("RAILS_ENV=production bin/mail_room start") sudo_gitlab("RAILS_ENV=production bin/mail_room start")
) )
...@@ -729,9 +729,9 @@ namespace :gitlab do ...@@ -729,9 +729,9 @@ namespace :gitlab do
end end
if connected if connected
puts "yes".green puts "yes".color(:green)
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
"Check that the information in config/gitlab.yml is correct" "Check that the information in config/gitlab.yml is correct"
) )
...@@ -799,7 +799,7 @@ namespace :gitlab do ...@@ -799,7 +799,7 @@ namespace :gitlab do
namespace :user do namespace :user do
desc "GitLab | Check the integrity of a specific user's repositories" desc "GitLab | Check the integrity of a specific user's repositories"
task :check_repos, [:username] => :environment do |t, args| task :check_repos, [:username] => :environment do |t, args|
username = args[:username] || prompt("Check repository integrity for which username? ".blue) username = args[:username] || prompt("Check repository integrity for which username? ".color(:blue))
user = User.find_by(username: username) user = User.find_by(username: username)
if user if user
repo_dirs = user.authorized_projects.map do |p| repo_dirs = user.authorized_projects.map do |p|
...@@ -811,7 +811,7 @@ namespace :gitlab do ...@@ -811,7 +811,7 @@ namespace :gitlab do
repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) } repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) }
else else
puts "\nUser '#{username}' not found".red puts "\nUser '#{username}' not found".color(:red)
end end
end end
end end
...@@ -820,13 +820,13 @@ namespace :gitlab do ...@@ -820,13 +820,13 @@ namespace :gitlab do
########################## ##########################
def fix_and_rerun def fix_and_rerun
puts " Please #{"fix the error above"} and rerun the checks.".red puts " Please #{"fix the error above"} and rerun the checks.".color(:red)
end end
def for_more_information(*sources) def for_more_information(*sources)
sources = sources.shift if sources.first.is_a?(Array) sources = sources.shift if sources.first.is_a?(Array)
puts " For more information see:".blue puts " For more information see:".color(:blue)
sources.each do |source| sources.each do |source|
puts " #{source}" puts " #{source}"
end end
...@@ -834,7 +834,7 @@ namespace :gitlab do ...@@ -834,7 +834,7 @@ namespace :gitlab do
def finished_checking(component) def finished_checking(component)
puts "" puts ""
puts "Checking #{component.yellow} ... #{"Finished".green}" puts "Checking #{component.color(:yellow)} ... #{"Finished".color(:green)}"
puts "" puts ""
end end
...@@ -855,14 +855,14 @@ namespace :gitlab do ...@@ -855,14 +855,14 @@ namespace :gitlab do
end end
def start_checking(component) def start_checking(component)
puts "Checking #{component.yellow} ..." puts "Checking #{component.color(:yellow)} ..."
puts "" puts ""
end end
def try_fixing_it(*steps) def try_fixing_it(*steps)
steps = steps.shift if steps.first.is_a?(Array) steps = steps.shift if steps.first.is_a?(Array)
puts " Try fixing it:".blue puts " Try fixing it:".color(:blue)
steps.each do |step| steps.each do |step|
puts " #{step}" puts " #{step}"
end end
...@@ -874,9 +874,9 @@ namespace :gitlab do ...@@ -874,9 +874,9 @@ namespace :gitlab do
print "GitLab Shell version >= #{required_version} ? ... " print "GitLab Shell version >= #{required_version} ? ... "
if current_version.valid? && required_version <= current_version if current_version.valid? && required_version <= current_version
puts "OK (#{current_version})".green puts "OK (#{current_version})".color(:green)
else else
puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".red puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".color(:red)
end end
end end
...@@ -887,9 +887,9 @@ namespace :gitlab do ...@@ -887,9 +887,9 @@ namespace :gitlab do
print "Ruby version >= #{required_version} ? ... " print "Ruby version >= #{required_version} ? ... "
if current_version.valid? && required_version <= current_version if current_version.valid? && required_version <= current_version
puts "yes (#{current_version})".green puts "yes (#{current_version})".color(:green)
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
"Update your ruby to a version >= #{required_version} from #{current_version}" "Update your ruby to a version >= #{required_version} from #{current_version}"
) )
...@@ -905,9 +905,9 @@ namespace :gitlab do ...@@ -905,9 +905,9 @@ namespace :gitlab do
print "Git version >= #{required_version} ? ... " print "Git version >= #{required_version} ? ... "
if current_version.valid? && required_version <= current_version if current_version.valid? && required_version <= current_version
puts "yes (#{current_version})".green puts "yes (#{current_version})".color(:green)
else else
puts "no".red puts "no".color(:red)
try_fixing_it( try_fixing_it(
"Update your git to a version >= #{required_version} from #{current_version}" "Update your git to a version >= #{required_version} from #{current_version}"
) )
...@@ -925,9 +925,9 @@ namespace :gitlab do ...@@ -925,9 +925,9 @@ namespace :gitlab do
def sanitized_message(project) def sanitized_message(project)
if should_sanitize? if should_sanitize?
"#{project.namespace_id.to_s.yellow}/#{project.id.to_s.yellow} ... " "#{project.namespace_id.to_s.color(:yellow)}/#{project.id.to_s.color(:yellow)} ... "
else else
"#{project.name_with_namespace.yellow} ... " "#{project.name_with_namespace.color(:yellow)} ... "
end end
end end
...@@ -940,7 +940,7 @@ namespace :gitlab do ...@@ -940,7 +940,7 @@ namespace :gitlab do
end end
def check_repo_integrity(repo_dir) def check_repo_integrity(repo_dir)
puts "\nChecking repo at #{repo_dir.yellow}" puts "\nChecking repo at #{repo_dir.color(:yellow)}"
git_fsck(repo_dir) git_fsck(repo_dir)
check_config_lock(repo_dir) check_config_lock(repo_dir)
...@@ -948,25 +948,25 @@ namespace :gitlab do ...@@ -948,25 +948,25 @@ namespace :gitlab do
end end
def git_fsck(repo_dir) def git_fsck(repo_dir)
puts "Running `git fsck`".yellow puts "Running `git fsck`".color(:yellow)
system(*%W(#{Gitlab.config.git.bin_path} fsck), chdir: repo_dir) system(*%W(#{Gitlab.config.git.bin_path} fsck), chdir: repo_dir)
end end
def check_config_lock(repo_dir) def check_config_lock(repo_dir)
config_exists = File.exist?(File.join(repo_dir,'config.lock')) config_exists = File.exist?(File.join(repo_dir,'config.lock'))
config_output = config_exists ? 'yes'.red : 'no'.green config_output = config_exists ? 'yes'.color(:red) : 'no'.color(:green)
puts "'config.lock' file exists?".yellow + " ... #{config_output}" puts "'config.lock' file exists?".color(:yellow) + " ... #{config_output}"
end end
def check_ref_locks(repo_dir) def check_ref_locks(repo_dir)
lock_files = Dir.glob(File.join(repo_dir,'refs/heads/*.lock')) lock_files = Dir.glob(File.join(repo_dir,'refs/heads/*.lock'))
if lock_files.present? if lock_files.present?
puts "Ref lock files exist:".red puts "Ref lock files exist:".color(:red)
lock_files.each do |lock_file| lock_files.each do |lock_file|
puts " #{lock_file}" puts " #{lock_file}"
end end
else else
puts "No ref lock files exist".green puts "No ref lock files exist".color(:green)
end end
end end
end end
...@@ -10,7 +10,7 @@ namespace :gitlab do ...@@ -10,7 +10,7 @@ namespace :gitlab do
git_base_path = Gitlab.config.gitlab_shell.repos_path git_base_path = Gitlab.config.gitlab_shell.repos_path
all_dirs = Dir.glob(git_base_path + '/*') all_dirs = Dir.glob(git_base_path + '/*')
puts git_base_path.yellow puts git_base_path.color(:yellow)
puts "Looking for directories to remove... " puts "Looking for directories to remove... "
all_dirs.reject! do |dir| all_dirs.reject! do |dir|
...@@ -29,17 +29,17 @@ namespace :gitlab do ...@@ -29,17 +29,17 @@ namespace :gitlab do
if remove_flag if remove_flag
if FileUtils.rm_rf dir_path if FileUtils.rm_rf dir_path
puts "Removed...#{dir_path}".red puts "Removed...#{dir_path}".color(:red)
else else
puts "Cannot remove #{dir_path}".red puts "Cannot remove #{dir_path}".color(:red)
end end
else else
puts "Can be removed: #{dir_path}".red puts "Can be removed: #{dir_path}".color(:red)
end end
end end
unless remove_flag unless remove_flag
puts "To cleanup this directories run this command with REMOVE=true".yellow puts "To cleanup this directories run this command with REMOVE=true".color(:yellow)
end end
end end
...@@ -75,19 +75,19 @@ namespace :gitlab do ...@@ -75,19 +75,19 @@ namespace :gitlab do
next unless user.ldap_user? next unless user.ldap_user?
print "#{user.name} (#{user.ldap_identity.extern_uid}) ..." print "#{user.name} (#{user.ldap_identity.extern_uid}) ..."
if Gitlab::LDAP::Access.allowed?(user) if Gitlab::LDAP::Access.allowed?(user)
puts " [OK]".green puts " [OK]".color(:green)
else else
if block_flag if block_flag
user.block! unless user.blocked? user.block! unless user.blocked?
puts " [BLOCKED]".red puts " [BLOCKED]".color(:red)
else else
puts " [NOT IN LDAP]".yellow puts " [NOT IN LDAP]".color(:yellow)
end end
end end
end end
unless block_flag unless block_flag
puts "To block these users run this command with BLOCK=true".yellow puts "To block these users run this command with BLOCK=true".color(:yellow)
end end
end end
end end
......
...@@ -3,22 +3,22 @@ namespace :gitlab do ...@@ -3,22 +3,22 @@ namespace :gitlab do
desc 'GitLab | Manually insert schema migration version' desc 'GitLab | Manually insert schema migration version'
task :mark_migration_complete, [:version] => :environment do |_, args| task :mark_migration_complete, [:version] => :environment do |_, args|
unless args[:version] unless args[:version]
puts "Must specify a migration version as an argument".red puts "Must specify a migration version as an argument".color(:red)
exit 1 exit 1
end end
version = args[:version].to_i version = args[:version].to_i
if version == 0 if version == 0
puts "Version '#{args[:version]}' must be a non-zero integer".red puts "Version '#{args[:version]}' must be a non-zero integer".color(:red)
exit 1 exit 1
end end
sql = "INSERT INTO schema_migrations (version) VALUES (#{version})" sql = "INSERT INTO schema_migrations (version) VALUES (#{version})"
begin begin
ActiveRecord::Base.connection.execute(sql) ActiveRecord::Base.connection.execute(sql)
puts "Successfully marked '#{version}' as complete".green puts "Successfully marked '#{version}' as complete".color(:green)
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
puts "Migration version '#{version}' is already marked complete".yellow puts "Migration version '#{version}' is already marked complete".color(:yellow)
end end
end end
......
...@@ -5,7 +5,7 @@ namespace :gitlab do ...@@ -5,7 +5,7 @@ namespace :gitlab do
task repack: :environment do task repack: :environment do
failures = perform_git_cmd(%W(git repack -a --quiet), "Repacking repo") failures = perform_git_cmd(%W(git repack -a --quiet), "Repacking repo")
if failures.empty? if failures.empty?
puts "Done".green puts "Done".color(:green)
else else
output_failures(failures) output_failures(failures)
end end
...@@ -15,7 +15,7 @@ namespace :gitlab do ...@@ -15,7 +15,7 @@ namespace :gitlab do
task gc: :environment do task gc: :environment do
failures = perform_git_cmd(%W(git gc --auto --quiet), "Garbage Collecting") failures = perform_git_cmd(%W(git gc --auto --quiet), "Garbage Collecting")
if failures.empty? if failures.empty?
puts "Done".green puts "Done".color(:green)
else else
output_failures(failures) output_failures(failures)
end end
...@@ -25,7 +25,7 @@ namespace :gitlab do ...@@ -25,7 +25,7 @@ namespace :gitlab do
task prune: :environment do task prune: :environment do
failures = perform_git_cmd(%W(git prune), "Git Prune") failures = perform_git_cmd(%W(git prune), "Git Prune")
if failures.empty? if failures.empty?
puts "Done".green puts "Done".color(:green)
else else
output_failures(failures) output_failures(failures)
end end
...@@ -47,7 +47,7 @@ namespace :gitlab do ...@@ -47,7 +47,7 @@ namespace :gitlab do
end end
def output_failures(failures) def output_failures(failures)
puts "The following repositories reported errors:".red puts "The following repositories reported errors:".color(:red)
failures.each { |f| puts "- #{f}" } failures.each { |f| puts "- #{f}" }
end end
......
...@@ -23,7 +23,7 @@ namespace :gitlab do ...@@ -23,7 +23,7 @@ namespace :gitlab do
group_name, name = File.split(path) group_name, name = File.split(path)
group_name = nil if group_name == '.' group_name = nil if group_name == '.'
puts "Processing #{repo_path}".yellow puts "Processing #{repo_path}".color(:yellow)
if path.end_with?('.wiki') if path.end_with?('.wiki')
puts " * Skipping wiki repo" puts " * Skipping wiki repo"
...@@ -51,9 +51,9 @@ namespace :gitlab do ...@@ -51,9 +51,9 @@ namespace :gitlab do
group.path = group_name group.path = group_name
group.owner = user group.owner = user
if group.save if group.save
puts " * Created Group #{group.name} (#{group.id})".green puts " * Created Group #{group.name} (#{group.id})".color(:green)
else else
puts " * Failed trying to create group #{group.name}".red puts " * Failed trying to create group #{group.name}".color(:red)
end end
end end
# set project group # set project group
...@@ -63,17 +63,17 @@ namespace :gitlab do ...@@ -63,17 +63,17 @@ namespace :gitlab do
project = Projects::CreateService.new(user, project_params).execute project = Projects::CreateService.new(user, project_params).execute
if project.persisted? if project.persisted?
puts " * Created #{project.name} (#{repo_path})".green puts " * Created #{project.name} (#{repo_path})".color(:green)
project.update_repository_size project.update_repository_size
project.update_commit_count project.update_commit_count
else else
puts " * Failed trying to create #{project.name} (#{repo_path})".red puts " * Failed trying to create #{project.name} (#{repo_path})".color(:red)
puts " Errors: #{project.errors.messages}".red puts " Errors: #{project.errors.messages}".color(:red)
end end
end end
end end
puts "Done!".green puts "Done!".color(:green)
end end
end end
end end
...@@ -15,15 +15,15 @@ namespace :gitlab do ...@@ -15,15 +15,15 @@ namespace :gitlab do
rake_version = run_and_match(%W(rake --version), /[\d\.]+/).try(:to_s) rake_version = run_and_match(%W(rake --version), /[\d\.]+/).try(:to_s)
puts "" puts ""
puts "System information".yellow puts "System information".color(:yellow)
puts "System:\t\t#{os_name || "unknown".red}" puts "System:\t\t#{os_name || "unknown".color(:red)}"
puts "Current User:\t#{run(%W(whoami))}" puts "Current User:\t#{run(%W(whoami))}"
puts "Using RVM:\t#{rvm_version.present? ? "yes".green : "no"}" puts "Using RVM:\t#{rvm_version.present? ? "yes".color(:green) : "no"}"
puts "RVM Version:\t#{rvm_version}" if rvm_version.present? puts "RVM Version:\t#{rvm_version}" if rvm_version.present?
puts "Ruby Version:\t#{ruby_version || "unknown".red}" puts "Ruby Version:\t#{ruby_version || "unknown".color(:red)}"
puts "Gem Version:\t#{gem_version || "unknown".red}" puts "Gem Version:\t#{gem_version || "unknown".color(:red)}"
puts "Bundler Version:#{bunder_version || "unknown".red}" puts "Bundler Version:#{bunder_version || "unknown".color(:red)}"
puts "Rake Version:\t#{rake_version || "unknown".red}" puts "Rake Version:\t#{rake_version || "unknown".color(:red)}"
puts "Sidekiq Version:#{Sidekiq::VERSION}" puts "Sidekiq Version:#{Sidekiq::VERSION}"
...@@ -39,7 +39,7 @@ namespace :gitlab do ...@@ -39,7 +39,7 @@ namespace :gitlab do
omniauth_providers.map! { |provider| provider['name'] } omniauth_providers.map! { |provider| provider['name'] }
puts "" puts ""
puts "GitLab information".yellow puts "GitLab information".color(:yellow)
puts "Version:\t#{Gitlab::VERSION}" puts "Version:\t#{Gitlab::VERSION}"
puts "Revision:\t#{Gitlab::REVISION}" puts "Revision:\t#{Gitlab::REVISION}"
puts "Directory:\t#{Rails.root}" puts "Directory:\t#{Rails.root}"
...@@ -47,9 +47,9 @@ namespace :gitlab do ...@@ -47,9 +47,9 @@ namespace :gitlab do
puts "URL:\t\t#{Gitlab.config.gitlab.url}" puts "URL:\t\t#{Gitlab.config.gitlab.url}"
puts "HTTP Clone URL:\t#{http_clone_url}" puts "HTTP Clone URL:\t#{http_clone_url}"
puts "SSH Clone URL:\t#{ssh_clone_url}" puts "SSH Clone URL:\t#{ssh_clone_url}"
puts "Using LDAP:\t#{Gitlab.config.ldap.enabled ? "yes".green : "no"}" puts "Using LDAP:\t#{Gitlab.config.ldap.enabled ? "yes".color(:green) : "no"}"
puts "Using Omniauth:\t#{Gitlab.config.omniauth.enabled ? "yes".green : "no"}" puts "Using Omniauth:\t#{Gitlab.config.omniauth.enabled ? "yes".color(:green) : "no"}"
puts "Omniauth Providers: #{omniauth_providers.map(&:magenta).join(', ')}" if Gitlab.config.omniauth.enabled puts "Omniauth Providers: #{omniauth_providers.join(', ')}" if Gitlab.config.omniauth.enabled
...@@ -60,8 +60,8 @@ namespace :gitlab do ...@@ -60,8 +60,8 @@ namespace :gitlab do
end end
puts "" puts ""
puts "GitLab Shell".yellow puts "GitLab Shell".color(:yellow)
puts "Version:\t#{gitlab_shell_version || "unknown".red}" puts "Version:\t#{gitlab_shell_version || "unknown".color(:red)}"
puts "Repositories:\t#{Gitlab.config.gitlab_shell.repos_path}" puts "Repositories:\t#{Gitlab.config.gitlab_shell.repos_path}"
puts "Hooks:\t\t#{Gitlab.config.gitlab_shell.hooks_path}" puts "Hooks:\t\t#{Gitlab.config.gitlab_shell.hooks_path}"
puts "Git:\t\t#{Gitlab.config.git.bin_path}" puts "Git:\t\t#{Gitlab.config.git.bin_path}"
......
...@@ -118,12 +118,12 @@ namespace :gitlab do ...@@ -118,12 +118,12 @@ namespace :gitlab do
puts "" puts ""
unless $?.success? unless $?.success?
puts "Failed to add keys...".red puts "Failed to add keys...".color(:red)
exit 1 exit 1
end end
rescue Gitlab::TaskAbortedByUserError rescue Gitlab::TaskAbortedByUserError
puts "Quitting...".red puts "Quitting...".color(:red)
exit 1 exit 1
end end
......
...@@ -2,7 +2,7 @@ module Gitlab ...@@ -2,7 +2,7 @@ module Gitlab
class TaskAbortedByUserError < StandardError; end class TaskAbortedByUserError < StandardError; end
end end
String.disable_colorization = true unless STDOUT.isatty require 'rainbow/ext/string'
# Prevent StateMachine warnings from outputting during a cron task # Prevent StateMachine warnings from outputting during a cron task
StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON'] StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON']
...@@ -14,7 +14,7 @@ namespace :gitlab do ...@@ -14,7 +14,7 @@ namespace :gitlab do
# Returns "yes" the user chose to continue # Returns "yes" the user chose to continue
# Raises Gitlab::TaskAbortedByUserError if the user chose *not* to continue # Raises Gitlab::TaskAbortedByUserError if the user chose *not* to continue
def ask_to_continue def ask_to_continue
answer = prompt("Do you want to continue (yes/no)? ".blue, %w{yes no}) answer = prompt("Do you want to continue (yes/no)? ".color(:blue), %w{yes no})
raise Gitlab::TaskAbortedByUserError unless answer == "yes" raise Gitlab::TaskAbortedByUserError unless answer == "yes"
end end
...@@ -98,10 +98,10 @@ namespace :gitlab do ...@@ -98,10 +98,10 @@ namespace :gitlab do
gitlab_user = Gitlab.config.gitlab.user gitlab_user = Gitlab.config.gitlab.user
current_user = run(%W(whoami)).chomp current_user = run(%W(whoami)).chomp
unless current_user == gitlab_user unless current_user == gitlab_user
puts " Warning ".colorize(:black).on_yellow puts " Warning ".color(:black).background(:yellow)
puts " You are running as user #{current_user.magenta}, we hope you know what you are doing." puts " You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing."
puts " Things may work\/fail for the wrong reasons." puts " Things may work\/fail for the wrong reasons."
puts " For correct results you should run this as user #{gitlab_user.magenta}." puts " For correct results you should run this as user #{gitlab_user.color(:magenta)}."
puts "" puts ""
end end
@warned_user_not_gitlab = true @warned_user_not_gitlab = true
......
...@@ -6,17 +6,17 @@ namespace :gitlab do ...@@ -6,17 +6,17 @@ namespace :gitlab do
count = scope.count count = scope.count
if count > 0 if count > 0
puts "This will disable 2FA for #{count.to_s.red} users..." puts "This will disable 2FA for #{count.to_s.color(:red)} users..."
begin begin
ask_to_continue ask_to_continue
scope.find_each(&:disable_two_factor!) scope.find_each(&:disable_two_factor!)
puts "Successfully disabled 2FA for #{count} users.".green puts "Successfully disabled 2FA for #{count} users.".color(:green)
rescue Gitlab::TaskAbortedByUserError rescue Gitlab::TaskAbortedByUserError
puts "Quitting...".red puts "Quitting...".color(:red)
end end
else else
puts "There are currently no users with 2FA enabled.".yellow puts "There are currently no users with 2FA enabled.".color(:yellow)
end end
end end
end end
......
...@@ -6,15 +6,15 @@ namespace :gitlab do ...@@ -6,15 +6,15 @@ namespace :gitlab do
ask_to_continue unless ENV['force'] == 'yes' ask_to_continue unless ENV['force'] == 'yes'
projects.find_each(batch_size: 100) do |project| projects.find_each(batch_size: 100) do |project|
print "#{project.name_with_namespace.yellow} ... " print "#{project.name_with_namespace.color(:yellow)} ... "
unless project.repo_exists? unless project.repo_exists?
puts "skipping, because the repo is empty".magenta puts "skipping, because the repo is empty".color(:magenta)
next next
end end
project.update_commit_count project.update_commit_count
puts project.commit_count.to_s.green puts project.commit_count.to_s.color(:green)
end end
end end
end end
...@@ -2,14 +2,14 @@ namespace :gitlab do ...@@ -2,14 +2,14 @@ namespace :gitlab do
desc "GitLab | Update gitignore" desc "GitLab | Update gitignore"
task :update_gitignore do task :update_gitignore do
unless clone_gitignores unless clone_gitignores
puts "Cloning the gitignores failed".red puts "Cloning the gitignores failed".color(:red)
return return
end end
remove_unneeded_files(gitignore_directory) remove_unneeded_files(gitignore_directory)
remove_unneeded_files(global_directory) remove_unneeded_files(global_directory)
puts "Done".green puts "Done".color(:green)
end end
def clone_gitignores def clone_gitignores
......
...@@ -12,9 +12,9 @@ namespace :gitlab do ...@@ -12,9 +12,9 @@ namespace :gitlab do
print "- #{project.name} ... " print "- #{project.name} ... "
web_hook = project.hooks.new(url: web_hook_url) web_hook = project.hooks.new(url: web_hook_url)
if web_hook.save if web_hook.save
puts "added".green puts "added".color(:green)
else else
print "failed".red print "failed".color(:red)
puts " [#{web_hook.errors.full_messages.to_sentence}]" puts " [#{web_hook.errors.full_messages.to_sentence}]"
end end
end end
...@@ -57,7 +57,7 @@ namespace :gitlab do ...@@ -57,7 +57,7 @@ namespace :gitlab do
if namespace if namespace
Project.in_namespace(namespace.id) Project.in_namespace(namespace.id)
else else
puts "Namespace not found: #{namespace_path}".red puts "Namespace not found: #{namespace_path}".color(:red)
exit 2 exit 2
end end
end end
......
desc "GitLab | Build internal ids for issues and merge requests" desc "GitLab | Build internal ids for issues and merge requests"
task migrate_iids: :environment do task migrate_iids: :environment do
puts 'Issues'.yellow puts 'Issues'.color(:yellow)
Issue.where(iid: nil).find_each(batch_size: 100) do |issue| Issue.where(iid: nil).find_each(batch_size: 100) do |issue|
begin begin
issue.set_iid issue.set_iid
...@@ -15,7 +15,7 @@ task migrate_iids: :environment do ...@@ -15,7 +15,7 @@ task migrate_iids: :environment do
end end
puts 'done' puts 'done'
puts 'Merge Requests'.yellow puts 'Merge Requests'.color(:yellow)
MergeRequest.where(iid: nil).find_each(batch_size: 100) do |mr| MergeRequest.where(iid: nil).find_each(batch_size: 100) do |mr|
begin begin
mr.set_iid mr.set_iid
...@@ -30,7 +30,7 @@ task migrate_iids: :environment do ...@@ -30,7 +30,7 @@ task migrate_iids: :environment do
end end
puts 'done' puts 'done'
puts 'Milestones'.yellow puts 'Milestones'.color(:yellow)
Milestone.where(iid: nil).find_each(batch_size: 100) do |m| Milestone.where(iid: nil).find_each(batch_size: 100) do |m|
begin begin
m.set_iid m.set_iid
......
...@@ -52,7 +52,7 @@ def run_spinach_tests(tags) ...@@ -52,7 +52,7 @@ def run_spinach_tests(tags)
tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp) tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp)
puts '' puts ''
puts "Spinach tests for #{tags}: Retrying tests... #{tests}".red puts "Spinach tests for #{tags}: Retrying tests... #{tests}".color(:red)
puts '' puts ''
sleep(3) sleep(3)
success = run_spinach_command(tests) success = run_spinach_command(tests)
......
...@@ -31,9 +31,9 @@ describe GroupsController do ...@@ -31,9 +31,9 @@ describe GroupsController do
let(:issue_2) { create(:issue, project: project) } let(:issue_2) { create(:issue, project: project) }
before do before do
create_list(:upvote_note, 3, project: project, noteable: issue_2) create_list(:award_emoji, 3, awardable: issue_2)
create_list(:upvote_note, 2, project: project, noteable: issue_1) create_list(:award_emoji, 2, awardable: issue_1)
create_list(:downvote_note, 2, project: project, noteable: issue_2) create_list(:award_emoji, 2, :downvote, awardable: issue_2,)
sign_in(user) sign_in(user)
end end
...@@ -56,9 +56,9 @@ describe GroupsController do ...@@ -56,9 +56,9 @@ describe GroupsController do
let(:merge_request_2) { create(:merge_request, :simple, source_project: project) } let(:merge_request_2) { create(:merge_request, :simple, source_project: project) }
before do before do
create_list(:upvote_note, 3, project: project, noteable: merge_request_2) create_list(:award_emoji, 3, awardable: merge_request_2)
create_list(:upvote_note, 2, project: project, noteable: merge_request_1) create_list(:award_emoji, 2, awardable: merge_request_1)
create_list(:downvote_note, 2, project: project, noteable: merge_request_2) create_list(:award_emoji, 2, :downvote, awardable: merge_request_2)
sign_in(user) sign_in(user)
end end
......
...@@ -250,4 +250,20 @@ describe Projects::IssuesController do ...@@ -250,4 +250,20 @@ describe Projects::IssuesController do
end end
end end
end end
describe 'POST #toggle_award_emoji' do
before do
sign_in(user)
project.team << [user, :developer]
end
it "toggles the award emoji" do
expect do
post(:toggle_award_emoji, namespace_id: project.namespace.path,
project_id: project.path, id: issue.iid, name: "thumbsup")
end.to change { issue.award_emoji.count }.by(1)
expect(response.status).to eq(200)
end
end
end end
FactoryGirl.define do
factory :award_emoji do
name "thumbsup"
user
awardable factory: :issue
trait :upvote
trait :downvote do
name "thumbsdown"
end
end
end
...@@ -16,8 +16,6 @@ FactoryGirl.define do ...@@ -16,8 +16,6 @@ FactoryGirl.define do
factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff], class: LegacyDiffNote factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff], class: LegacyDiffNote
factory :note_on_project_snippet, traits: [:on_project_snippet] factory :note_on_project_snippet, traits: [:on_project_snippet]
factory :system_note, traits: [:system] factory :system_note, traits: [:system]
factory :downvote_note, traits: [:award, :downvote]
factory :upvote_note, traits: [:award, :upvote]
trait :on_commit do trait :on_commit do
noteable nil noteable nil
...@@ -46,10 +44,6 @@ FactoryGirl.define do ...@@ -46,10 +44,6 @@ FactoryGirl.define do
system true system true
end end
trait :award do
is_award true
end
trait :downvote do trait :downvote do
note "thumbsdown" note "thumbsdown"
end end
......
...@@ -28,7 +28,6 @@ describe 'Awards Emoji', feature: true do ...@@ -28,7 +28,6 @@ describe 'Awards Emoji', feature: true do
end end
context 'click the thumbsup emoji' do context 'click the thumbsup emoji' do
it 'should increment the thumbsup emoji', js: true do it 'should increment the thumbsup emoji', js: true do
find('[data-emoji="thumbsup"]').click find('[data-emoji="thumbsup"]').click
sleep 2 sleep 2
...@@ -41,7 +40,6 @@ describe 'Awards Emoji', feature: true do ...@@ -41,7 +40,6 @@ describe 'Awards Emoji', feature: true do
end end
context 'click the thumbsdown emoji' do context 'click the thumbsdown emoji' do
it 'should increment the thumbsdown emoji', js: true do it 'should increment the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click find('[data-emoji="thumbsdown"]').click
sleep 2 sleep 2
......
require 'rails_helper'
feature 'Issue awards', js: true, feature: true do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
describe 'logged in' do
before do
login_as(user)
visit namespace_project_issue_path(project.namespace, project, issue)
end
it 'should add award to issue' do
first('.js-emoji-btn').click
expect(page).to have_selector('.js-emoji-btn.active')
expect(first('.js-emoji-btn')).to have_content '1'
visit namespace_project_issue_path(project.namespace, project, issue)
expect(first('.js-emoji-btn')).to have_content '1'
end
it 'should remove award from issue' do
first('.js-emoji-btn').click
find('.js-emoji-btn.active').click
expect(first('.js-emoji-btn')).to have_content '0'
visit namespace_project_issue_path(project.namespace, project, issue)
expect(first('.js-emoji-btn')).to have_content '0'
end
it 'should only have one menu on the page' do
first('.js-add-award').click
expect(page).to have_selector('.emoji-menu')
expect(page).to have_selector('.emoji-menu', count: 1)
end
end
describe 'logged out' do
before do
visit namespace_project_issue_path(project.namespace, project, issue)
end
it 'should not see award menu button' do
expect(page).not_to have_selector('.js-award-holder')
end
end
end
require 'rails_helper'
feature 'Issues > Labels bulk assignment', feature: true do
include WaitForAjax
let(:user) { create(:user) }
let!(:project) { create(:project) }
let!(:issue1) { create(:issue, project: project, title: "Issue 1") }
let!(:issue2) { create(:issue, project: project, title: "Issue 2") }
let!(:bug) { create(:label, project: project, title: 'bug') }
let!(:feature) { create(:label, project: project, title: 'feature') }
context 'as a allowed user', js: true do
before do
project.team << [user, :master]
login_as user
end
context 'can bulk assign' do
before do
visit namespace_project_issues_path(project.namespace, project)
end
context 'a label' do
context 'to all issues' do
before do
check 'check_all_issues'
open_labels_dropdown ['bug']
update_issues
end
it do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).to have_content 'bug'
end
end
context 'to a issue' do
before do
check "selected_issue_#{issue1.id}"
open_labels_dropdown ['bug']
update_issues
end
it do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
end
end
end
context 'multiple labels' do
context 'to all issues' do
before do
check 'check_all_issues'
open_labels_dropdown ['bug', 'feature']
update_issues
end
it do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue1.id}")).to have_content 'feature'
expect(find("#issue_#{issue2.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
end
end
context 'to a issue' do
before do
check "selected_issue_#{issue1.id}"
open_labels_dropdown ['bug', 'feature']
update_issues
end
it do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue1.id}")).to have_content 'feature'
expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
expect(find("#issue_#{issue2.id}")).not_to have_content 'feature'
end
end
end
end
context 'can bulk un-assign' do
context 'all labels to all issues' do
before do
issue1.labels << bug
issue1.labels << feature
issue2.labels << bug
issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project)
check 'check_all_issues'
unmark_labels_in_dropdown ['bug', 'feature']
update_issues
end
it do
expect(find("#issue_#{issue1.id}")).not_to have_content 'bug'
expect(find("#issue_#{issue1.id}")).not_to have_content 'feature'
expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
expect(find("#issue_#{issue2.id}")).not_to have_content 'feature'
end
end
context 'a label to a issue' do
before do
issue1.labels << bug
issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project)
check_issue issue1
unmark_labels_in_dropdown ['bug']
update_issues
end
it do
expect(find("#issue_#{issue1.id}")).not_to have_content 'bug'
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
end
end
context 'a label and keep the others label' do
before do
issue1.labels << bug
issue1.labels << feature
issue2.labels << bug
issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project)
check_issue issue1
check_issue issue2
unmark_labels_in_dropdown ['bug']
update_issues
end
it do
expect(find("#issue_#{issue1.id}")).not_to have_content 'bug'
expect(find("#issue_#{issue1.id}")).to have_content 'feature'
expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
end
end
end
end
context 'as a guest' do
before do
login_as user
visit namespace_project_issues_path(project.namespace, project)
end
context 'cannot bulk assign labels' do
it do
expect(page).not_to have_css '.check_all_issues'
expect(page).not_to have_css '.issue-check'
end
end
end
def open_labels_dropdown(items = [], unmark = false)
page.within('.issues_bulk_update') do
click_button 'Label'
wait_for_ajax
items.map do |item|
click_link item
end
if unmark
items.map do |item|
click_link item
end
end
end
end
def unmark_labels_in_dropdown(items = [])
open_labels_dropdown(items, true)
end
def check_issue(issue)
page.within('.issues-list') do
check "selected_issue_#{issue.id}"
end
end
def update_issues
click_button 'Update issues'
wait_for_ajax
end
end
require 'rails_helper' require 'rails_helper'
feature 'Multiple issue updating from issues#index', feature: true do feature 'Multiple issue updating from issues#index', feature: true do
include WaitForAjax
let!(:project) { create(:project) } let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) } let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)} let!(:user) { create(:user)}
...@@ -24,9 +26,7 @@ feature 'Multiple issue updating from issues#index', feature: true do ...@@ -24,9 +26,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
it 'should be set to open' do it 'should be set to open' do
create_closed create_closed
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project.namespace, project, state: 'closed')
find('.issues-state-filters a', text: 'Closed').click
find('#check_all_issues').click find('#check_all_issues').click
find('.js-issue-status').click find('.js-issue-status').click
...@@ -42,7 +42,7 @@ feature 'Multiple issue updating from issues#index', feature: true do ...@@ -42,7 +42,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click find('#check_all_issues').click
find('.js-update-assignee').click click_update_assignee_button
find('.dropdown-menu-user-link', text: user.username).click find('.dropdown-menu-user-link', text: user.username).click
click_update_issues_button click_update_issues_button
...@@ -57,14 +57,11 @@ feature 'Multiple issue updating from issues#index', feature: true do ...@@ -57,14 +57,11 @@ feature 'Multiple issue updating from issues#index', feature: true do
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click find('#check_all_issues').click
find('.js-update-assignee').click click_update_assignee_button
click_link 'Unassigned' click_link 'Unassigned'
click_update_issues_button click_update_issues_button
expect(find('.issue:first-child .controls')).not_to have_css('.author_link')
within first('.issue .controls') do
expect(page).to have_no_selector('.author_link')
end
end end
end end
...@@ -95,7 +92,7 @@ feature 'Multiple issue updating from issues#index', feature: true do ...@@ -95,7 +92,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
find('.dropdown-menu-milestone a', text: "No Milestone").click find('.dropdown-menu-milestone a', text: "No Milestone").click
click_update_issues_button click_update_issues_button
expect(first('.issue')).not_to have_content milestone.title expect(find('.issue:first-child')).not_to have_content milestone.title
end end
end end
...@@ -111,7 +108,13 @@ feature 'Multiple issue updating from issues#index', feature: true do ...@@ -111,7 +108,13 @@ feature 'Multiple issue updating from issues#index', feature: true do
create(:issue, project: project, milestone: milestone) create(:issue, project: project, milestone: milestone)
end end
def click_update_assignee_button
find('.js-update-assignee').click
wait_for_ajax
end
def click_update_issues_button def click_update_issues_button
find('.update_selected_issues').click find('.update_selected_issues').click
wait_for_ajax
end end
end end
...@@ -125,7 +125,7 @@ describe 'Issues', feature: true do ...@@ -125,7 +125,7 @@ describe 'Issues', feature: true do
describe 'Issue info' do describe 'Issue info' do
it 'excludes award_emoji from comment count' do it 'excludes award_emoji from comment count' do
issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar') issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar')
create(:upvote_note, noteable: issue, project: project) create(:award_emoji, awardable: issue)
visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id) visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id)
......
require 'rails_helper'
feature 'Merge request awards', js: true, feature: true do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request, source_project: project) }
describe 'logged in' do
before do
login_as(user)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
it 'should add award to merge request' do
first('.js-emoji-btn').click
expect(page).to have_selector('.js-emoji-btn.active')
expect(first('.js-emoji-btn')).to have_content '1'
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
expect(first('.js-emoji-btn')).to have_content '1'
end
it 'should remove award from merge request' do
first('.js-emoji-btn').click
find('.js-emoji-btn.active').click
expect(first('.js-emoji-btn')).to have_content '0'
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
expect(first('.js-emoji-btn')).to have_content '0'
end
it 'should only have one menu on the page' do
first('.js-add-award').click
expect(page).to have_selector('.emoji-menu')
expect(page).to have_selector('.emoji-menu', count: 1)
end
end
describe 'logged out' do
before do
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
it 'should not see award menu button' do
expect(page).not_to have_selector('.js-award-holder')
end
end
end
...@@ -4,20 +4,6 @@ describe 'Comments', feature: true do ...@@ -4,20 +4,6 @@ describe 'Comments', feature: true do
include RepoHelpers include RepoHelpers
include WaitForAjax include WaitForAjax
describe 'On merge requests page', feature: true do
it 'excludes award_emoji from comment count' do
merge_request = create(:merge_request)
project = merge_request.source_project
create(:upvote_note, noteable: merge_request, project: project)
login_as :admin
visit namespace_project_merge_requests_path(project.namespace, project)
expect(merge_request.mr_and_commit_notes.count).to eq 1
expect(page.all('.merge-request-no-comments').first.text).to eq "0"
end
end
describe 'On a merge request', js: true, feature: true do describe 'On a merge request', js: true, feature: true do
let!(:project) { create(:project) } let!(:project) { create(:project) }
let!(:merge_request) do let!(:merge_request) do
...@@ -147,17 +133,6 @@ describe 'Comments', feature: true do ...@@ -147,17 +133,6 @@ describe 'Comments', feature: true do
end end
end end
end end
describe 'comment info' do
it 'excludes award_emoji from comment count' do
create(:upvote_note, noteable: merge_request, project: project)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
expect(merge_request.mr_and_commit_notes.count).to eq 2
expect(find('.notes-tab span.badge').text).to eq "1"
end
end
end end
describe 'On a merge request diff', js: true, feature: true do describe 'On a merge request diff', js: true, feature: true do
......
...@@ -163,18 +163,15 @@ describe IssuesHelper do ...@@ -163,18 +163,15 @@ describe IssuesHelper do
it { is_expected.to eq("!1, !2, or !3") } it { is_expected.to eq("!1, !2, or !3") }
end end
describe "note_active_class" do describe '#award_active_class' do
before do let!(:upvote) { create(:award_emoji) }
@note = create :note
@note1 = create :note
end
it "returns empty string for unauthenticated user" do it "returns empty string for unauthenticated user" do
expect(note_active_class(Note.all, nil)).to eq("") expect(award_active_class(AwardEmoji.all, nil)).to eq("")
end end
it "returns active string for author" do it "returns active string for author" do
expect(note_active_class(Note.all, @note.author)).to eq("active") expect(award_active_class(AwardEmoji.all, upvote.user)).to eq("active")
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe AwardEmoji do describe Gitlab::AwardEmoji do
describe '.urls' do describe '.urls' do
subject { AwardEmoji.urls } subject { Gitlab::AwardEmoji.urls }
it { is_expected.to be_an_instance_of(Array) } it { is_expected.to be_an_instance_of(Array) }
it { is_expected.not_to be_empty } it { is_expected.not_to be_empty }
...@@ -19,7 +19,7 @@ describe AwardEmoji do ...@@ -19,7 +19,7 @@ describe AwardEmoji do
describe '.emoji_by_category' do describe '.emoji_by_category' do
it "only contains known categories" do it "only contains known categories" do
undefined_categories = AwardEmoji.emoji_by_category.keys - AwardEmoji::CATEGORIES.keys undefined_categories = Gitlab::AwardEmoji.emoji_by_category.keys - Gitlab::AwardEmoji::CATEGORIES.keys
expect(undefined_categories).to be_empty expect(undefined_categories).to be_empty
end end
end end
......
require 'spec_helper'
describe AwardEmoji, models: true do
describe 'Associations' do
it { is_expected.to belong_to(:awardable) }
it { is_expected.to belong_to(:user) }
end
describe 'modules' do
it { is_expected.to include_module(Participable) }
end
describe "validations" do
it { is_expected.to validate_presence_of(:awardable) }
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:name) }
# To circumvent a bug in the shoulda matchers
describe "scoped uniqueness validation" do
it "rejects duplicate award emoji" do
user = create(:user)
issue = create(:issue)
create(:award_emoji, user: user, awardable: issue)
new_award = build(:award_emoji, user: user, awardable: issue)
expect(new_award).not_to be_valid
end
end
end
end
require 'spec_helper'
describe Issue, "Awardable" do
let!(:issue) { create(:issue) }
let!(:award_emoji) { create(:award_emoji, :downvote, awardable: issue) }
describe "Associations" do
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
end
describe "ClassMethods" do
let!(:issue2) { create(:issue) }
before do
create(:award_emoji, awardable: issue2)
end
it "orders on upvotes" do
expect(Issue.order_upvotes_desc.to_a).to eq [issue2, issue]
end
it "orders on downvotes" do
expect(Issue.order_downvotes_desc.to_a).to eq [issue, issue2]
end
end
describe "#upvotes" do
it "counts the number of upvotes" do
expect(issue.upvotes).to be 0
end
end
describe "#downvotes" do
it "counts the number of downvotes" do
expect(issue.downvotes).to be 1
end
end
describe "#toggle_award_emoji" do
it "adds an emoji if it isn't awarded yet" do
expect { issue.toggle_award_emoji("thumbsup", award_emoji.user) }.to change { AwardEmoji.count }.by(1)
end
it "toggles already awarded emoji" do
expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by(-1)
end
end
end
...@@ -12,6 +12,10 @@ describe Issue, "Issuable" do ...@@ -12,6 +12,10 @@ describe Issue, "Issuable" do
it { is_expected.to have_many(:todos).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) }
end end
describe 'Included modules' do
it { is_expected.to include_module(Awardable) }
end
describe "Validation" do describe "Validation" do
before do before do
allow(subject).to receive(:set_iid).and_return(false) allow(subject).to receive(:set_iid).and_return(false)
...@@ -245,8 +249,8 @@ describe Issue, "Issuable" do ...@@ -245,8 +249,8 @@ describe Issue, "Issuable" do
let(:project) { issue.project } let(:project) { issue.project }
before do before do
issue.notes.awards.create!(note: "thumbsup", author: user, project: project) create(:award_emoji, :upvote, awardable: issue)
issue.notes.awards.create!(note: "thumbsdown", author: user, project: project) create(:award_emoji, :downvote, awardable: issue)
end end
it "returns correct values" do it "returns correct values" do
......
...@@ -171,23 +171,6 @@ describe Note, models: true do ...@@ -171,23 +171,6 @@ describe Note, models: true do
end end
end end
describe '.grouped_awards' do
before do
create :note, note: "smile", is_award: true
create :note, note: "smile", is_award: true
end
it "returns grouped hash of notes" do
expect(Note.grouped_awards.keys.size).to eq(3)
expect(Note.grouped_awards["smile"]).to match_array(Note.all)
end
it "returns thumbsup and thumbsdown always" do
expect(Note.grouped_awards["thumbsup"]).to match_array(Note.none)
expect(Note.grouped_awards["thumbsdown"]).to match_array(Note.none)
end
end
describe "editable?" do describe "editable?" do
it "returns true" do it "returns true" do
note = build(:note) note = build(:note)
...@@ -198,11 +181,6 @@ describe Note, models: true do ...@@ -198,11 +181,6 @@ describe Note, models: true do
note = build(:note, system: true) note = build(:note, system: true)
expect(note.editable?).to be_falsy expect(note.editable?).to be_falsy
end end
it "returns false" do
note = build(:note, is_award: true, note: "smiley")
expect(note.editable?).to be_falsy
end
end end
describe "cross_reference_not_visible_for?" do describe "cross_reference_not_visible_for?" do
...@@ -229,29 +207,6 @@ describe Note, models: true do ...@@ -229,29 +207,6 @@ describe Note, models: true do
end end
end end
describe "set_award!" do
let(:merge_request) { create :merge_request }
it "converts aliases to actual name" do
note = create(:note, note: ":+1:",
noteable: merge_request,
project: merge_request.project)
expect(note.reload.note).to eq("thumbsup")
end
it "is not an award emoji when comment is on a diff" do
note = create(:note_on_merge_request_diff, note: ":blowfish:",
noteable: merge_request,
project: merge_request.project,
line_code: "11d5d2e667e9da4f7f610f81d86c974b146b13bd_0_2")
note = note.reload
expect(note.note).to eq(":blowfish:")
expect(note.is_award?).to be_falsy
end
end
describe 'clear_blank_line_code!' do describe 'clear_blank_line_code!' do
it 'clears a blank line code before validation' do it 'clears a blank line code before validation' do
note = build(:note, line_code: ' ') note = build(:note, line_code: ' ')
......
...@@ -30,6 +30,7 @@ describe User, models: true do ...@@ -30,6 +30,7 @@ describe User, models: true do
it { is_expected.to have_one(:abuse_report) } it { is_expected.to have_one(:abuse_report) }
it { is_expected.to have_many(:spam_logs).dependent(:destroy) } it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) }
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
end end
describe 'validations' do describe 'validations' do
......
...@@ -249,7 +249,6 @@ describe API::API, api: true do ...@@ -249,7 +249,6 @@ describe API::API, api: true do
expect(json_response['milestone']).to be_a Hash expect(json_response['milestone']).to be_a Hash
expect(json_response['assignee']).to be_a Hash expect(json_response['assignee']).to be_a Hash
expect(json_response['author']).to be_a Hash expect(json_response['author']).to be_a Hash
expect(json_response['user_notes_count']).to be(1)
end end
it "should return a project issue by id" do it "should return a project issue by id" do
......
...@@ -138,7 +138,6 @@ describe API::API, api: true do ...@@ -138,7 +138,6 @@ describe API::API, api: true do
expect(json_response['work_in_progress']).to be_falsy expect(json_response['work_in_progress']).to be_falsy
expect(json_response['merge_when_build_succeeds']).to be_falsy expect(json_response['merge_when_build_succeeds']).to be_falsy
expect(json_response['merge_status']).to eq('can_be_merged') expect(json_response['merge_status']).to eq('can_be_merged')
expect(json_response['user_notes_count']).to be(2)
end end
it "should return merge_request" do it "should return merge_request" do
......
require 'spec_helper' require 'spec_helper'
describe Issues::BulkUpdateService, services: true do describe Issues::BulkUpdateService, services: true do
let(:issue) { create(:issue, project: @project) } let(:user) { create(:user) }
let(:project) { Projects::CreateService.new(user, namespace: user.namespace, name: 'test').execute }
before do
@user = create :user
opts = {
name: "GitLab",
namespace: @user.namespace
}
@project = Projects::CreateService.new(@user, opts).execute
end
describe :close_issue do let!(:result) { Issues::BulkUpdateService.new(project, user, params).execute }
before do describe :close_issue do
@issues = create_list(:issue, 5, project: @project) let(:issues) { create_list(:issue, 5, project: project) }
@params = { let(:params) do
{
state_event: 'close', state_event: 'close',
issues_ids: @issues.map(&:id).join(",") issues_ids: issues.map(&:id).join(',')
} }
end end
it do it 'succeeds and returns the correct number of issues updated' do
result = Issues::BulkUpdateService.new(@project, @user, @params).execute
expect(result[:success]).to be_truthy expect(result[:success]).to be_truthy
expect(result[:count]).to eq(@issues.count) expect(result[:count]).to eq(issues.count)
expect(@project.issues.opened).to be_empty
expect(@project.issues.closed).not_to be_empty
end end
it 'closes all the issues passed' do
expect(project.issues.opened).to be_empty
expect(project.issues.closed).not_to be_empty
end
end end
describe :reopen_issues do describe :reopen_issues do
before do let(:issues) { create_list(:closed_issue, 5, project: project) }
@issues = create_list(:closed_issue, 5, project: @project) let(:params) do
@params = { {
state_event: 'reopen', state_event: 'reopen',
issues_ids: @issues.map(&:id).join(",") issues_ids: issues.map(&:id).join(',')
} }
end end
it do it 'succeeds and returns the correct number of issues updated' do
result = Issues::BulkUpdateService.new(@project, @user, @params).execute
expect(result[:success]).to be_truthy expect(result[:success]).to be_truthy
expect(result[:count]).to eq(@issues.count) expect(result[:count]).to eq(issues.count)
expect(@project.issues.closed).to be_empty
expect(@project.issues.opened).not_to be_empty
end end
it 'reopens all the issues passed' do
expect(project.issues.closed).to be_empty
expect(project.issues.opened).not_to be_empty
end
end end
describe :update_assignee do describe 'updating assignee' do
let(:issue) do
create(:issue, project: project) { |issue| issue.update_attributes(assignee: user) }
end
before do let(:params) do
@new_assignee = create :user {
@params = { assignee_id: assignee_id,
issues_ids: issue.id.to_s, issues_ids: issue.id.to_s
assignee_id: @new_assignee.id
} }
end end
it do context 'when the new assignee ID is a valid user' do
result = Issues::BulkUpdateService.new(@project, @user, @params).execute let(:new_assignee) { create(:user) }
expect(result[:success]).to be_truthy let(:assignee_id) { new_assignee.id }
expect(result[:count]).to eq(1)
expect(@project.issues.first.assignee).to eq(@new_assignee) it 'succeeds' do
end expect(result[:success]).to be_truthy
expect(result[:count]).to eq(1)
end
it 'allows mass-unassigning' do it 'updates the assignee to the use ID passed' do
@project.issues.first.update_attribute(:assignee, @new_assignee) expect(issue.reload.assignee).to eq(new_assignee)
expect(@project.issues.first.assignee).not_to be_nil end
end
@params[:assignee_id] = -1 context 'when the new assignee ID is -1' do
let(:assignee_id) { -1 }
Issues::BulkUpdateService.new(@project, @user, @params).execute it 'unassigns the issues' do
expect(@project.issues.first.assignee).to be_nil expect(issue.reload.assignee).to be_nil
end
end end
it 'does not unassign when assignee_id is not present' do context 'when the new assignee ID is not present' do
@project.issues.first.update_attribute(:assignee, @new_assignee) let(:assignee_id) { nil }
expect(@project.issues.first.assignee).not_to be_nil
@params[:assignee_id] = ''
Issues::BulkUpdateService.new(@project, @user, @params).execute it 'does not unassign' do
expect(@project.issues.first.assignee).not_to be_nil expect(issue.reload.assignee).to eq(user)
end
end end
end end
describe :update_milestone do describe 'updating milestones' do
let(:issue) { create(:issue, project: project) }
let(:milestone) { create(:milestone, project: project) }
before do let(:params) do
@milestone = create(:milestone, project: @project) {
@params = {
issues_ids: issue.id.to_s, issues_ids: issue.id.to_s,
milestone_id: @milestone.id milestone_id: milestone.id
} }
end end
it do it 'succeeds' do
result = Issues::BulkUpdateService.new(@project, @user, @params).execute
expect(result[:success]).to be_truthy expect(result[:success]).to be_truthy
expect(result[:count]).to eq(1) expect(result[:count]).to eq(1)
end
expect(@project.issues.first.milestone).to eq(@milestone) it 'updates the issue milestone' do
expect(project.issues.first.milestone).to eq(milestone)
end end
end end
describe 'updating labels' do
def create_issue_with_labels(labels)
create(:issue, project: project) { |issue| issue.update_attributes(labels: labels) }
end
let(:bug) { create(:label, project: project) }
let(:regression) { create(:label, project: project) }
let(:merge_requests) { create(:label, project: project) }
let(:issue_all_labels) { create_issue_with_labels([bug, regression, merge_requests]) }
let(:issue_bug_and_regression) { create_issue_with_labels([bug, regression]) }
let(:issue_bug_and_merge_requests) { create_issue_with_labels([bug, merge_requests]) }
let(:issue_no_labels) { create(:issue, project: project) }
let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests, issue_no_labels] }
let(:labels) { [] }
let(:add_labels) { [] }
let(:remove_labels) { [] }
let(:params) do
{
label_ids: labels.map(&:id),
add_label_ids: add_labels.map(&:id),
remove_label_ids: remove_labels.map(&:id),
issues_ids: issues.map(&:id).join(',')
}
end
context 'when label_ids are passed' do
let(:issues) { [issue_all_labels, issue_no_labels] }
let(:labels) { [bug, regression] }
it 'updates the labels of all issues passed to the labels passed' do
expect(issues.map(&:reload).map(&:label_ids)).to all(eq(labels.map(&:id)))
end
it 'does not update issues not passed in' do
expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
end
context 'when those label IDs are empty' do
let(:labels) { [] }
it 'updates the issues passed to have no labels' do
expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty)
end
end
end
context 'when add_label_ids are passed' do
let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] }
let(:add_labels) { [bug, regression, merge_requests] }
it 'adds those label IDs to all issues passed' do
expect(issues.map(&:reload).map(&:label_ids)).to all(include(*add_labels.map(&:id)))
end
it 'does not update issues not passed in' do
expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
end
end
context 'when remove_label_ids are passed' do
let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] }
let(:remove_labels) { [bug, regression, merge_requests] }
it 'removes those label IDs from all issues passed' do
expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty)
end
it 'does not update issues not passed in' do
expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
end
end
context 'when add_label_ids and remove_label_ids are passed' do
let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] }
let(:add_labels) { [bug] }
let(:remove_labels) { [merge_requests] }
it 'adds the label IDs to all issues passed' do
expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id))
end
it 'removes the label IDs from all issues passed' do
expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id)
end
it 'does not update issues not passed in' do
expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
end
end
context 'when add_label_ids and label_ids are passed' do
let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests] }
let(:labels) { [merge_requests] }
let(:add_labels) { [regression] }
it 'adds the label IDs to all issues passed' do
expect(issues.map(&:reload).map(&:label_ids)).to all(include(regression.id))
end
it 'ignores the label IDs parameter' do
expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id))
end
it 'does not update issues not passed in' do
expect(issue_no_labels.label_ids).to be_empty
end
end
context 'when remove_label_ids and label_ids are passed' do
let(:issues) { [issue_no_labels, issue_bug_and_regression] }
let(:labels) { [merge_requests] }
let(:remove_labels) { [regression] }
it 'remove the label IDs from all issues passed' do
expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id)
end
it 'ignores the label IDs parameter' do
expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id)
end
it 'does not update issues not passed in' do
expect(issue_all_labels.label_ids).to contain_exactly(bug.id, regression.id, merge_requests.id)
end
end
context 'when add_label_ids, remove_label_ids, and label_ids are passed' do
let(:issues) { [issue_bug_and_merge_requests, issue_no_labels] }
let(:labels) { [regression] }
let(:add_labels) { [bug] }
let(:remove_labels) { [merge_requests] }
it 'adds the label IDs to all issues passed' do
expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id))
end
it 'removes the label IDs from all issues passed' do
expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id)
end
it 'ignores the label IDs parameter' do
expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id)
end
it 'does not update issues not passed in' do
expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
end
end
end
end end
...@@ -39,6 +39,7 @@ describe Issues::MoveService, services: true do ...@@ -39,6 +39,7 @@ describe Issues::MoveService, services: true do
let!(:milestone2) do let!(:milestone2) do
create(:milestone, project_id: new_project.id, title: 'v9.0') create(:milestone, project_id: new_project.id, title: 'v9.0')
end end
let!(:award_emoji) { create(:award_emoji, awardable: old_issue) }
let!(:new_issue) { move_service.execute(old_issue, new_project) } let!(:new_issue) { move_service.execute(old_issue, new_project) }
end end
...@@ -115,6 +116,10 @@ describe Issues::MoveService, services: true do ...@@ -115,6 +116,10 @@ describe Issues::MoveService, services: true do
it 'preserves create time' do it 'preserves create time' do
expect(old_issue.created_at).to eq new_issue.created_at expect(old_issue.created_at).to eq new_issue.created_at
end end
it 'moves the award emoji' do
expect(old_issue.award_emoji.first.name).to eq new_issue.reload.award_emoji.first.name
end
end end
context 'issue with notes' do context 'issue with notes' do
......
# coding: utf-8
require 'spec_helper' require 'spec_helper'
describe Issues::UpdateService, services: true do describe Issues::UpdateService, services: true do
...@@ -273,5 +274,50 @@ describe Issues::UpdateService, services: true do ...@@ -273,5 +274,50 @@ describe Issues::UpdateService, services: true do
end end
end end
end end
context 'updating labels' do
let(:label3) { create(:label, project: project) }
let(:result) { Issues::UpdateService.new(project, user, params).execute(issue).reload }
context 'when add_label_ids and label_ids are passed' do
let(:params) { { label_ids: [label.id], add_label_ids: [label3.id] } }
it 'ignores the label_ids parameter' do
expect(result.label_ids).not_to include(label.id)
end
it 'adds the passed labels' do
expect(result.label_ids).to include(label3.id)
end
end
context 'when remove_label_ids and label_ids are passed' do
let(:params) { { label_ids: [], remove_label_ids: [label.id] } }
before { issue.update_attributes(labels: [label, label3]) }
it 'ignores the label_ids parameter' do
expect(result.label_ids).not_to be_empty
end
it 'removes the passed labels' do
expect(result.label_ids).not_to include(label.id)
end
end
context 'when add_label_ids and remove_label_ids are passed' do
let(:params) { { add_label_ids: [label3.id], remove_label_ids: [label.id] } }
before { issue.update_attributes(labels: [label]) }
it 'adds the passed labels' do
expect(result.label_ids).to include(label3.id)
end
it 'removes the passed labels' do
expect(result.label_ids).not_to include(label.id)
end
end
end
end end
end end
...@@ -14,7 +14,7 @@ describe Notes::CreateService, services: true do ...@@ -14,7 +14,7 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue', noteable_type: 'Issue',
noteable_id: issue.id noteable_id: issue.id
} }
@note = Notes::CreateService.new(project, user, opts).execute @note = Notes::CreateService.new(project, user, opts).execute
end end
...@@ -28,18 +28,16 @@ describe Notes::CreateService, services: true do ...@@ -28,18 +28,16 @@ describe Notes::CreateService, services: true do
project.team << [user, :master] project.team << [user, :master]
end end
it "creates emoji note" do it "creates an award emoji" do
opts = { opts = {
note: ':smile: ', note: ':smile: ',
noteable_type: 'Issue', noteable_type: 'Issue',
noteable_id: issue.id noteable_id: issue.id
} }
note = Notes::CreateService.new(project, user, opts).execute
@note = Notes::CreateService.new(project, user, opts).execute expect(note).to be_valid
expect(note.name).to eq('smile')
expect(@note).to be_valid
expect(@note.note).to eq('smile')
expect(@note.is_award).to be_truthy
end end
it "creates regular note if emoji name is invalid" do it "creates regular note if emoji name is invalid" do
...@@ -48,12 +46,22 @@ describe Notes::CreateService, services: true do ...@@ -48,12 +46,22 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue', noteable_type: 'Issue',
noteable_id: issue.id noteable_id: issue.id
} }
note = Notes::CreateService.new(project, user, opts).execute
expect(note).to be_valid
expect(note.note).to eq(opts[:note])
end
it "normalizes the emoji name" do
opts = {
note: ':+1:',
noteable_type: 'Issue',
noteable_id: issue.id
}
@note = Notes::CreateService.new(project, user, opts).execute expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user)
expect(@note).to be_valid Notes::CreateService.new(project, user, opts).execute
expect(@note.note).to eq(opts[:note])
expect(@note.is_award).to be_falsy
end end
end end
end end
...@@ -156,7 +156,6 @@ describe TodoService, services: true do ...@@ -156,7 +156,6 @@ describe TodoService, services: true do
let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) } let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) }
let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) } let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) }
let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) } let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) }
let(:award_note) { create(:note, :award, project: project, noteable: issue, author: john_doe, note: 'thumbsup') }
let(:system_note) { create(:system_note, project: project, noteable: issue) } let(:system_note) { create(:system_note, project: project, noteable: issue) }
it 'mark related pending todos to the noteable for the note author as done' do it 'mark related pending todos to the noteable for the note author as done' do
...@@ -169,13 +168,6 @@ describe TodoService, services: true do ...@@ -169,13 +168,6 @@ describe TodoService, services: true do
expect(second_todo.reload).to be_done expect(second_todo.reload).to be_done
end end
it 'mark related pending todos to the noteable for the award note author as done' do
service.new_note(award_note, john_doe)
expect(first_todo.reload).to be_done
expect(second_todo.reload).to be_done
end
it 'does not mark related pending todos it is a system note' do it 'does not mark related pending todos it is a system note' do
service.new_note(system_note, john_doe) service.new_note(system_note, john_doe)
...@@ -306,6 +298,15 @@ describe TodoService, services: true do ...@@ -306,6 +298,15 @@ describe TodoService, services: true do
end end
end end
describe '#new_award_emoji' do
it 'marks related pending todos to the target for the user as done' do
todo = create(:todo, user: john_doe, project: project, target: mr_assigned, author: author)
service.new_award_emoji(mr_assigned, john_doe)
expect(todo.reload).to be_done
end
end
describe '#merge_request_build_failed' do describe '#merge_request_build_failed' do
it 'creates a pending todo for the merge request author' do it 'creates a pending todo for the merge request author' do
service.merge_request_build_failed(mr_unassigned) service.merge_request_build_failed(mr_unassigned)
......
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