Commit d225de32 authored by Kamil Trzcinski's avatar Kamil Trzcinski

Merge branch 'rename-ci-commit-phase-2' into rename-ci-commit-phase-3

parents ce2dd42e 842f0a34
......@@ -194,7 +194,7 @@ Style/EmptyLines:
# Keep blank lines around access modifiers.
Style/EmptyLinesAroundAccessModifier:
Enabled: false
Enabled: true
# Keeps track of empty lines around block bodies.
Style/EmptyLinesAroundBlockBody:
......@@ -771,7 +771,7 @@ Metrics/PerceivedComplexity:
# Checks for ambiguous operators in the first argument of a method invocation
# without parentheses.
Lint/AmbiguousOperator:
Enabled: false
Enabled: true
# Checks for ambiguous regexp literals in the first argument of a method
# invocation without parentheses.
......
Please view this file on the master branch, on stable branches it's out of date.
v 8.9.0 (unreleased)
- Bulk assign/unassign labels to issues.
- Allow enabling wiki page events from Webhook management UI
- Make EmailsOnPushWorker use Sidekiq mailers queue
- Fix wiki page events' webhook to point to the wiki repository
......@@ -35,13 +36,13 @@ v 8.9.0 (unreleased)
- Reduce number of queries needed to render issue labels in the sidebar
- Improve error handling importing projects
- Put project Files and Commits tabs under Code tab
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
- Replace Colorize with Rainbow for coloring console output in Rake tasks.
v 8.8.4 (unreleased)
- 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
- 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
[free Antetype viewer (Mac OSX only)] or grab an exported PNG from the design
(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
......@@ -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"
[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
[`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'
gem "httparty", '~> 0.13.3'
# Colored output to console
gem "colorize", '~> 0.7.0'
gem "rainbow", '~> 2.1.0'
# GitLab settings
gem 'settingslogic', '~> 2.0.9'
......
......@@ -823,7 +823,6 @@ DEPENDENCIES
carrierwave (~> 0.10.0)
charlock_holmes (~> 0.7.3)
coffee-rails (~> 4.1.0)
colorize (~> 0.7.0)
connection_pool (~> 2.0)
coveralls (~> 0.8.2)
creole (~> 0.5.0)
......@@ -914,6 +913,7 @@ DEPENDENCIES
rack-oauth2 (~> 1.2.1)
rails (= 4.2.6)
rails-deprecated_sanitizer (~> 1.0.3)
rainbow (~> 2.1.0)
raphael-rails (~> 2.1.2)
rblineprof
rdoc (~> 3.6)
......
......@@ -17,11 +17,13 @@ class Dispatcher
switch page
when 'projects:issues:index'
Issuable.init()
new IssuableBulkActions()
shortcut_handler = new ShortcutsNavigation()
when 'projects:issues:show'
new Issue()
shortcut_handler = new ShortcutsIssuable()
new ZenMode()
window.awardsHandler = new AwardsHandler()
when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone()
when 'dashboard:todos:index'
......@@ -52,6 +54,7 @@ class Dispatcher
new Diff()
shortcut_handler = new ShortcutsIssuable(true)
new ZenMode()
window.awardsHandler = new AwardsHandler()
when "projects:merge_requests:diffs"
new Diff()
new ZenMode()
......
class @Flash
constructor: (message, type)->
constructor: (message, type = 'alert')->
@flash = $(".flash-container")
@flash.html("")
......
......@@ -11,6 +11,8 @@ class GitLabDropdownFilter
$inputContainer = @input.parent()
$clearButton = $inputContainer.find('.js-dropdown-input-clear')
@indeterminateIds = []
# Clear click
$clearButton.on 'click', (e) =>
e.preventDefault()
......@@ -35,20 +37,20 @@ class GitLabDropdownFilter
if keyCode is 13
return false
clearTimeout timeout
timeout = setTimeout =>
blur_field = @shouldBlur keyCode
search_text = @input.val()
# Only filter asynchronously only if option remote is set
if @options.remote
clearTimeout timeout
timeout = setTimeout =>
blur_field = @shouldBlur keyCode
if blur_field and @filterInputBlur
@input.blur()
if blur_field and @filterInputBlur
@input.blur()
if @options.remote
@options.query search_text, (data) =>
@options.query @input.val(), (data) =>
@options.callback(data)
else
@filter search_text
, 250
, 250
else
@filter @input.val()
shouldBlur: (keyCode) ->
return BLUR_KEYCODES.indexOf(keyCode) >= 0
......@@ -142,6 +144,7 @@ class GitLabDropdown
LOADING_CLASS = "is-loading"
PAGE_TWO_CLASS = "is-page-two"
ACTIVE_CLASS = "is-active"
INDETERMINATE_CLASS = "is-indeterminate"
currentIndex = -1
FILTER_INPUT = '.dropdown-input .dropdown-input-field'
......@@ -182,9 +185,6 @@ class GitLabDropdown
@fullData = data
@parseData @fullData
if @options.filterable
@filterInput.trigger 'keyup'
}
# Init filterable
......@@ -298,6 +298,13 @@ class GitLabDropdown
opened: =>
@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()
if @remote && contentHtml is ""
@remote.execute()
......@@ -309,12 +316,18 @@ class GitLabDropdown
hidden: (e) =>
@removeArrayKeyEvent()
$input = @dropdown.find(".dropdown-input-field")
if @options.filterable
@dropdown
.find(".dropdown-input-field")
$input
.blur()
.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
$('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
......@@ -358,7 +371,7 @@ class GitLabDropdown
if @options.renderRow
# Call the render function
html = @options.renderRow(data)
html = @options.renderRow.call(@options, data, @)
else
if not selected
value = if @options.id then @options.id(data) else data.id
......@@ -443,6 +456,17 @@ class GitLabDropdown
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel
else
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
if not @options.multiSelect or el.hasClass('dropdown-clear-active')
@dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
......@@ -459,31 +483,42 @@ class GitLabDropdown
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el)
if value?
if !field.length and fieldName
# Create hidden input for form
input = "<input type='hidden' name='#{fieldName}' value='#{value}' />"
if @options.inputId?
input = $(input)
.attr('id', @options.inputId)
@dropdown.before input
@addInput(fieldName, value)
else
field.val value
return selectedObject
selectRowAtIndex: (index) ->
selector = ".dropdown-content li:not(.divider):eq(#{index}) a"
addInput: (fieldName, value)->
# 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
selector = ".dropdown-page-one #{selector}"
# 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: ->
ARROW_KEY_CODES = [38, 40]
$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
selector = ".dropdown-page-one #{selector}"
......@@ -511,8 +546,8 @@ class GitLabDropdown
return false
if currentKeyCode is 13
@selectRowAtIndex if currentIndex < 0 then 0 else currentIndex
if currentKeyCode is 13 and currentIndex isnt -1
@selectRowAtIndex e, currentIndex
removeArrayKeyEvent: ->
$('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
constructor: ->
_this = @
$('.js-label-select').each (i, dropdown) ->
$dropdown = $(dropdown)
projectId = $dropdown.data('project-id')
......@@ -196,10 +198,18 @@ class @LabelsSelect
callback data
renderRow: (label) ->
removesAll = label.id is 0 or not label.id?
renderRow: (label, instance) ->
$li = $('<li>')
$a = $('<a href="#">')
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']\
[name='#{$dropdown.data('fieldName')}']\
[value='#{this.id(label)}']").length
......@@ -230,13 +240,17 @@ class @LabelsSelect
else
colorEl = ''
"<li>
<a href='#' class='#{selectedClass.join(' ')}'>
#{colorEl}
#{_.escape(label.title)}
</a>
</li>"
filterable: true
# We need to identify which items are actually labels
if label.id
selectedClass.push('label-item')
$a.attr('data-label-id', label.id)
$a.addClass(selectedClass.join(' '))
.html("#{colorEl} #{_.escape(label.title)}")
# Return generated html
$li.html($a).prop('outerHTML')
persistWhenHide: $dropdown.data('persistWhenHide')
search:
fields: ['title']
selectable: true
......@@ -280,10 +294,19 @@ class @LabelsSelect
else if $dropdown.hasClass('js-filter-submit')
$dropdown.closest('form').submit()
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'
clicked: (label) ->
if $dropdown.hasClass('js-filter-bulk-update')
return
page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is 'projects:merge_requests:index'
......@@ -298,4 +321,31 @@ class @LabelsSelect
return
else
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
return
if note.award
awardsHandler.addAwardToEmojiBar(note.note)
awardsHandler.addAwardToEmojiBar(note.name)
awardsHandler.scrollToAwards()
# render note if it not present in loaded list
......
......@@ -156,11 +156,14 @@ class @SearchAutocomplete
# No need to enable anything if user is not logged in
return if !gon.current_user_id
_this = @
@loadingSuggestions = false
unless @dropdown.hasClass('open')
_this = @
@loadingSuggestions = false
@dropdown.addClass('open')
@searchInput.removeClass('disabled')
@dropdown
.addClass('open')
.trigger('shown.bs.dropdown')
@searchInput.removeClass('disabled')
onSearchInputKeyDown: =>
# Saves last length of the entered text
......@@ -191,7 +194,7 @@ class @SearchAutocomplete
@disableAutocomplete()
else
# 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
......
......@@ -232,9 +232,8 @@
a {
padding-left: 25px;
&.is-active {
&.is-indeterminate, &.is-active {
&::before {
content: "\f00c";
position: absolute;
left: 5px;
top: 50%;
......@@ -246,6 +245,14 @@
-moz-osx-font-smoothing: grayscale;
}
}
&.is-indeterminate::before {
content: "\f068";
}
&.is-active::before {
content: "\f00c";
}
}
}
......
......@@ -2,18 +2,10 @@
* Generic mixins
*/
@mixin box-shadow($shadow) {
-webkit-box-shadow: $shadow;
-moz-box-shadow: $shadow;
-ms-box-shadow: $shadow;
-o-box-shadow: $shadow;
box-shadow: $shadow;
}
@mixin border-radius($radius) {
-webkit-border-radius: $radius;
-moz-border-radius: $radius;
-ms-border-radius: $radius;
-o-border-radius: $radius;
border-radius: $radius;
}
......
.awards {
line-height: 34px;
.emoji-icon {
width: 20px;
height: 20px;
......@@ -9,8 +7,6 @@
.emoji-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: 3px;
z-index: 1000;
min-width: 160px;
......@@ -23,7 +19,12 @@
opacity: 0;
transform: scale(.2);
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 {
pointer-events: all;
......@@ -107,7 +108,7 @@
}
&.is-loading {
.award-control-icon {
.award-control-icon-normal {
display: none;
}
......
......@@ -3,12 +3,7 @@
background: #111;
color: #fff;
font-family: $monospace_font;
white-space: pre;
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+ */
white-space: pre-wrap;
overflow: auto;
overflow-y: hidden;
font-size: 12px;
......
......@@ -158,13 +158,11 @@
.search-holder {
@media (min-width: $screen-sm-min) {
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.search-field-holder {
-webkit-flex: 1 0 auto;
-ms-flex: 1 0 auto;
flex: 1 0 auto;
position: relative;
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
include ToggleSubscriptionAction
include IssuableActions
include ToggleAwardEmoji
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
......@@ -62,7 +63,7 @@ class Projects::IssuesController < Projects::ApplicationController
def show
@note = @project.notes.new(noteable: @issue)
@notes = @issue.notes.nonawards.with_associations.fresh
@notes = @issue.notes.with_associations.fresh
@noteable = @issue
respond_to do |format|
......@@ -155,7 +156,12 @@ class Projects::IssuesController < Projects::ApplicationController
def bulk_update
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
protected
......@@ -169,6 +175,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
alias_method :subscribable_resource, :issue
alias_method :issuable, :issue
alias_method :awardable, :issue
def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue)
......@@ -214,7 +221,10 @@ class Projects::IssuesController < Projects::ApplicationController
:issues_ids,
:assignee_id,
:milestone_id,
:state_event
:state_event,
label_ids: [],
add_label_ids: [],
remove_label_ids: []
)
end
end
......@@ -2,6 +2,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
include ToggleSubscriptionAction
include DiffHelper
include IssuableActions
include ToggleAwardEmoji
before_action :module_enabled
before_action :merge_request, only: [
......@@ -201,7 +202,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
if params[:merge_when_build_succeeds].present? && @merge_request.pipeline && @merge_request.pipeline.active?
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
.execute(@merge_request)
.execute(@merge_request)
@status = :merge_when_build_succeeds
else
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
......@@ -270,6 +271,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
alias_method :subscribable_resource, :merge_request
alias_method :issuable, :merge_request
alias_method :awardable, :merge_request
def closes_issues
@closes_issues ||= @merge_request.closes_issues
......@@ -305,7 +307,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_show_vars
# Build a note object for comment form
@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
@noteable = @merge_request
......
......@@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create]
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
current_fetched_at = Time.now.to_i
......@@ -56,30 +56,6 @@ class Projects::NotesController < Projects::ApplicationController
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
def note
......@@ -131,13 +107,20 @@ class Projects::NotesController < Projects::ApplicationController
end
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,
id: note.id,
discussion_id: note.discussion_id,
html: note_to_html(note),
award: note.is_award,
award: false,
note: note.note,
discussion_html: note_to_discussion_html(note),
discussion_with_diff_html: note_to_discussion_with_diff_html(note)
......@@ -145,7 +128,7 @@ class Projects::NotesController < Projects::ApplicationController
else
{
valid: false,
award: note.is_award,
award: false,
errors: note.errors
}
end
......
......@@ -139,7 +139,7 @@ class ProjectsController < Projects::ApplicationController
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
@suggestions = {
emojis: AwardEmoji.urls,
emojis: Gitlab::AwardEmoji.urls,
issues: autocomplete.issues,
milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests,
......
......@@ -12,9 +12,9 @@ class NotesFinder
when "commit"
project.notes.for_commit_id(target_id).non_diff_notes
when "issue"
project.issues.find(target_id).notes.nonawards.inc_author
project.issues.find(target_id).notes.inc_author
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"
project.snippets.find(target_id).notes
else
......
......@@ -96,5 +96,4 @@ module IssuablesHelper
issuable.open? ? :opened : :closed
end
end
end
......@@ -145,16 +145,14 @@ module IssuesHelper
end
end
def emoji_author_list(notes, current_user)
list = notes.map do |note|
note.author == current_user ? "me" : note.author.name
end
list.join(", ")
def award_user_list(awards, current_user)
awards.map do |award|
award.user == current_user ? 'me' : award.user.name
end.join(', ')
end
def note_active_class(notes, current_user)
if current_user && notes.pluck(:author_id).include?(current_user.id)
def award_active_class(awards, current_user)
if current_user && awards.find { |a| a.user_id == current_user.id }
"active"
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
include Mentionable
include Subscribable
include StripAttribute
include Awardable
included do
belongs_to :author, class_name: "User"
......@@ -115,29 +116,6 @@ module Issuable
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)
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}")
......@@ -179,14 +157,6 @@ module Issuable
opened? || reopened?
end
def downvotes
notes.awards.where(note: "thumbsdown").count
end
def upvotes
notes.awards.where(note: "thumbsup").count
end
def user_notes_count
notes.user.count
end
......
......@@ -110,6 +110,10 @@ class LegacyDiffNote < Note
@active
end
def award_emoji_supported?
false
end
private
def find_diff
......
......@@ -21,11 +21,8 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true
delegate :title, to: :noteable, allow_nil: true
before_validation :set_award!
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
validates :attachment, file_size: { maximum: :max_attachment_size }
......@@ -43,8 +40,6 @@ class Note < ActiveRecord::Base
mount_uploader :attachment, AttachmentUploader
# 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 :system, ->{ where(system: true) }
scope :user, ->{ where(system: false) }
......@@ -109,19 +104,6 @@ class Note < ActiveRecord::Base
found_notes.where('issues.confidential IS NULL OR issues.confidential IS FALSE')
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
def cross_reference?
......@@ -205,44 +187,24 @@ class Note < ActiveRecord::Base
Event.reset_event_cache_for(self)
end
def downvote?
is_award && note == "thumbsdown"
end
def upvote?
is_award && note == "thumbsup"
end
def editable?
!system? && !is_award
!system?
end
def cross_reference_not_visible_for?(user)
cross_reference? && referenced_mentionables(user).empty?
end
# Checks if note is an award added as a comment
#
# 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
def award_emoji?
award_emoji_supported? && contains_emoji_only?
end
private
def clear_blank_line_code!
self.line_code = nil if self.line_code.blank?
end
def awards_supported?
(for_issue? || for_merge_request?) && !diff_note?
def award_emoji_supported?
noteable.is_a?(Awardable)
end
def contains_emoji_only?
......@@ -251,6 +213,6 @@ class Note < ActiveRecord::Base
def award_emoji_name
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
......@@ -83,7 +83,7 @@ class IrkerService < Service
self.channels = recipients.split(/\s+/).map do |recipient|
format_channel(recipient)
end
channels.reject! &:nil?
channels.reject!(&:nil?)
end
def format_channel(recipient)
......
......@@ -84,6 +84,7 @@ class User < ActiveRecord::Base
has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy
has_many :award_emoji, as: :awardable, dependent: :destroy
#
# Validations
......
......@@ -45,6 +45,8 @@ class IssuableBaseService < BaseService
unless can?(current_user, ability, project)
params.delete(:milestone_id)
params.delete(:add_label_ids)
params.delete(:remove_label_ids)
params.delete(:label_ids)
params.delete(:assignee_id)
end
......@@ -67,10 +69,34 @@ class IssuableBaseService < BaseService
end
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] =
project.labels.where(id: params[:label_ids]).pluck(:id)
params[key] = project.labels.where(id: params[key]).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
def update(issuable)
......@@ -78,7 +104,7 @@ class IssuableBaseService < BaseService
filter_params
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
handle_common_system_notes(issuable, old_labels: old_labels)
handle_changes(issuable, old_labels: old_labels)
......
......@@ -4,9 +4,9 @@ module Issues
issues_ids = params.delete(:issues_ids).split(",")
issue_params = params
issue_params.delete(:state_event) unless issue_params[:state_event].present?
issue_params.delete(:milestone_id) unless issue_params[:milestone_id].present?
issue_params.delete(:assignee_id) unless issue_params[:assignee_id].present?
%i(state_event milestone_id assignee_id add_label_ids remove_label_ids).each do |key|
issue_params.delete(key) unless issue_params[key].present?
end
issues = Issue.where(id: issues_ids)
issues.each do |issue|
......
......@@ -24,6 +24,7 @@ module Issues
@new_issue = create_new_issue
rewrite_notes
rewrite_award_emoji
add_note_moved_from
# Old issue tasks
......@@ -72,6 +73,14 @@ module Issues
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)
return unless content
......
......@@ -5,6 +5,13 @@ module Notes
note.author = current_user
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
# Finish the harder work in the background
NewNoteWorker.perform_in(2.seconds, note.id, params)
......
......@@ -8,7 +8,7 @@ module Notes
def execute
# 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)
@note.create_cross_references!
execute_note_hooks
......
......@@ -130,8 +130,7 @@ class NotificationService
# ignore gitlab service messages
return true if note.note.start_with?('Status changed to closed')
return true if note.cross_reference? && note.system == true
return true if note.is_award
return true if note.cross_reference? && note.system?
target = note.noteable
......
......@@ -22,6 +22,7 @@ module Oauth2::AccessTokenValidationService
end
protected
# True if the token's scope is a superset of required scopes,
# or the required scopes is empty.
def sufficient_scope?(token, scopes)
......
......@@ -122,6 +122,14 @@ class TodoService
handle_note(note, current_user)
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:
#
# * mark all pending todos related to the target for the current user as done
......
......@@ -99,8 +99,8 @@
%td.build-link
- if project
= link_to ci_status_path(build.commit) do
%strong #{build.commit.short_sha}
= link_to ci_status_path(build.pipeline) do
%strong #{build.pipeline.short_sha}
%td.timestamp
- if build.finished_at
......
- 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 @@
.login-body
= 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.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.
.prepend-top-20
= f.submit "Verify code", class: "btn btn-save"
.emoji-menu
.emoji-menu-content
= 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
= AwardEmoji::CATEGORIES[category]
= Gitlab::AwardEmoji::CATEGORIES[category]
%ul.clearfix.emoji-menu-list
- emojis.each do |emoji|
%li.pull-left.text-center.emoji-menu-list-item
......
......@@ -51,7 +51,7 @@
= link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do
= icon('hdd-o fw')
%span
Container Registry
Registry
- if project_nav_tab? :graphs
= nav_link(controller: %w(graphs)) do
......
......@@ -10,7 +10,7 @@
%p
Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)}
%p
Author: #{@build.commit.git_author_name}
Author: #{@build.pipeline.git_author_name}
%p
Branch: #{@build.ref}
%p
......@@ -18,7 +18,7 @@
%p
Job: #{@build.name}
%p
Message: #{@build.commit.git_commit_message}
Message: #{@build.pipeline.git_commit_message}
%p
Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
Build failed for <%= @project.name %>
Status: <%= @build.status %>
Commit: <%= @build.commit.short_sha %>
Author: <%= @build.commit.git_author_name %>
Commit: <%= @build.pipeline.short_sha %>
Author: <%= @build.pipeline.git_author_name %>
Branch: <%= @build.ref %>
Stage: <%= @build.stage %>
Job: <%= @build.name %>
Message: <%= @build.commit.git_commit_message %>
Message: <%= @build.pipeline.git_commit_message %>
Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %>
......@@ -10,7 +10,7 @@
%p
Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)}
%p
Author: #{@build.commit.git_author_name}
Author: #{@build.pipeline.git_author_name}
%p
Branch: #{@build.ref}
%p
......@@ -18,7 +18,7 @@
%p
Job: #{@build.name}
%p
Message: #{@build.commit.git_commit_message}
Message: #{@build.pipeline.git_commit_message}
%p
Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
Build successful for <%= @project.name %>
Status: <%= @build.status %>
Commit: <%= @build.commit.short_sha %>
Author: <%= @build.commit.git_author_name %>
Commit: <%= @build.pipeline.short_sha %>
Author: <%= @build.pipeline.git_author_name %>
Branch: <%= @build.ref %>
Stage: <%= @build.stage %>
Job: <%= @build.name %>
Message: <%= @build.commit.git_commit_message %>
Message: <%= @build.pipeline.git_commit_message %>
Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %>
......@@ -4,7 +4,7 @@
.build-page
.row-content-block.top-block
Build ##{@build.id} for commit
%strong.monospace= link_to @build.commit.short_sha, ci_status_path(@build.commit)
%strong.monospace= link_to @build.pipeline.short_sha, ci_status_path(@build.pipeline)
from
= link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref)
- merge_request = @build.merge_request
......@@ -13,7 +13,7 @@
= link_to "merge request #{merge_request.to_reference}", merge_request_path(merge_request)
#up-build-trace
- builds = @build.commit.builds.latest.to_a
- builds = @build.pipeline.builds.latest.to_a
- if builds.size > 1
%ul.nav-links.no-top.no-bottom
- builds.each do |build|
......@@ -178,16 +178,16 @@
Commit
.pull-right
%small
= link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace"
= link_to @build.pipeline.short_sha, ci_status_path(@build.pipeline), class: "monospace"
%p
%span.attr-name Branch:
= link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref)
%p
%span.attr-name Author:
#{@build.commit.git_author_name}
#{@build.pipeline.git_author_name}
%p
%span.attr-name Message:
#{@build.commit.git_commit_message}
#{@build.pipeline.git_commit_message}
- if @build.tags.any?
.build-widget
......@@ -201,7 +201,7 @@
.build-widget
%h4.title #{pluralize(@builds.count(:id), "other build")} for
= succeed ":" do
= link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace"
= link_to @build.pipeline.short_sha, ci_status_path(@build.pipeline), class: "monospace"
%table.table.builds
- @builds.each_with_index do |build, i|
%tr.build
......
- status = commit.status
- status = pipeline.status
%tr.commit
%td.commit-link
= link_to namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: "ci-status ci-#{status}" do
= link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: "ci-status ci-#{status}" do
= ci_icon_for_status(status)
%strong ##{commit.id}
%strong ##{pipeline.id}
%td
%div.branch-commit
- if commit.ref
= link_to commit.ref, namespace_project_commits_path(@project.namespace, @project, commit.ref), class: "monospace"
- if pipeline.ref
= link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace"
&middot;
= link_to commit.short_sha, namespace_project_commit_path(@project.namespace, @project, commit.sha), class: "commit-id monospace"
= link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: "commit-id monospace"
&nbsp;
- if commit.tag?
- if pipeline.tag?
%span.label.label-primary tag
- elsif commit.latest?
- elsif pipeline.latest?
%span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest
- if commit.triggered?
- if pipeline.triggered?
%span.label.label-primary triggered
- if commit.yaml_errors.present?
%span.label.label-danger.has-tooltip{ title: "#{commit.yaml_errors}" } yaml invalid
- if commit.builds.any?(&:stuck?)
- if pipeline.yaml_errors.present?
%span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid
- if pipeline.builds.any?(&:stuck?)
%span.label.label-warning stuck
%p.commit-title
- if commit_data = commit.commit_data
- if commit_data = pipeline.commit_data
= link_to_gfm truncate(commit_data.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit_data.id), class: "commit-row-message"
- else
Cant find HEAD commit for this branch
- stages_status = commit.statuses.stages_status
- stages_status = pipeline.statuses.stages_status
- stages.each do |stage|
%td
- status = stages_status[stage]
- tooltip = "#{stage.titleize}: #{status || 'not found'}"
- if status
= link_to namespace_project_pipeline_path(@project.namespace, @project, commit.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
= link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
= ci_icon_for_status(status)
- else
.light.has-tooltip{ title: tooltip }
\-
%td
- if commit.started_at && commit.finished_at
- if pipeline.started_at && pipeline.finished_at
%p.duration
#{duration_in_words(commit.finished_at, commit.started_at)}
#{duration_in_words(pipeline.finished_at, pipeline.started_at)}
%td
.controls.hidden-xs.pull-right
- artifacts = commit.builds.latest.select { |b| b.artifacts? }
- artifacts = pipeline.builds.latest.select { |b| b.artifacts? }
- if artifacts.present?
.dropdown.inline.build-artifacts
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
......@@ -63,9 +63,9 @@
%span #{build.name}
- if can?(current_user, :update_pipeline, @project)
- if commit.retryable?
= link_to retry_namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: 'btn has-tooltip', title: "Retry", method: :post do
- if pipeline.retryable?
= link_to retry_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do
= icon("repeat")
- if commit.cancelable?
= link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do
- if pipeline.cancelable?
= link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do
= icon("remove")
%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)
.issue-check
= check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
......@@ -27,7 +27,7 @@
= icon('thumbs-down')
= downvotes
- note_count = issue.notes.user.nonawards.count
- note_count = issue.notes.user.count
%li
= link_to issue_path(issue, anchor: 'notes'), class: ('issue-no-comments' if note_count.zero?) do
= icon('comments')
......
......@@ -68,9 +68,9 @@
#related-branches{ data: { url: related_branches_namespace_project_issue_url(@project.namespace, @project, @issue) } }
// This element is filled in using JavaScript.
.content-block.content-block-small
= render 'new_branch'
= render 'votes/votes_block', votable: @issue
.content-block.content-block-small
= render 'new_branch'
= render 'award_emoji/awards_block', awardable: @issue, inline: true
%section.issuable-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
.pull-info-right
%span.append-right-20
= link_to_label(label, type: :merge_request) do
......
......@@ -35,7 +35,7 @@
= icon('thumbs-down')
= downvotes
- note_count = merge_request.mr_and_commit_notes.user.nonawards.count
- note_count = merge_request.mr_and_commit_notes.user.count
%li
= link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do
= icon('comments')
......
......@@ -6,4 +6,3 @@
- if @merge_requests.present?
= paginate @merge_requests, theme: "gitlab"
......@@ -49,7 +49,7 @@
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do
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
= link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
Commits
......@@ -67,7 +67,7 @@
.tab-content
#notes.notes.tab-pane.voting_notes
.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
%section.col-md-12
......
......@@ -31,7 +31,7 @@
- if controller.controller_name == 'issues'
.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
= 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
......@@ -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]" } })
.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 } })
.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 :state_event, params[:state_event]
.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].respond_to?('any?')
- params[:label_name].each do |label|
= hidden_field_tag "label_name[]", label, id: nil
.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
= h(multi_label_name(params[:label_name], "Label"))
= icon('chevron-down')
.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" }
- if can? current_user, :admin_label, @project and @project
= render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label", show_footer: show_footer, show_create: show_create }
- if show_create and @project and can?(current_user, :admin_label, @project)
= render partial: "shared/issuable/label_page_create"
= dropdown_loading
- 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')
.dropdown-page-one
= dropdown_title(title)
= dropdown_filter(filter_placeholder)
= dropdown_content
- if @project
- if @project && show_footer
= dropdown_footer do
%ul.dropdown-footer-list
- if can? current_user, :admin_label, @project
- if can?(current_user, :admin_label, @project)
%li
%a.dropdown-toggle-page{href: "#"}
Create new
%li
= link_to namespace_project_labels_path(@project.namespace, @project), :"data-is-link" => true do
- if can? current_user, :admin_label, @project
- if show_create && @project && can?(current_user, :admin_label, @project)
Manage labels
- else
View labels
= dropdown_loading
\ No newline at end of file
= dropdown_loading
.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 @@
# inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep )
# end
#
ActiveSupport::Inflector.inflections do |inflect|
inflect.uncountable %w(award_emoji)
end
......@@ -652,6 +652,7 @@ Rails.application.routes.draw do
post :cancel_merge_when_build_succeeds
get :ci_status
post :toggle_subscription
post :toggle_award_emoji
post :remove_wip
end
......@@ -727,6 +728,7 @@ Rails.application.routes.draw do
resources :issues, constraints: { id: /\d+/ } do
member do
post :toggle_subscription
post :toggle_award_emoji
get :referenced_merge_requests
get :related_branches
get :can_create_branch
......@@ -757,10 +759,6 @@ Rails.application.routes.draw do
member do
delete :delete_attachment
end
collection do
post :award_toggle
end
end
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
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
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|
t.text "message", null: false
t.datetime "starts_at"
......@@ -638,7 +650,6 @@ ActiveRecord::Schema.define(version: 20160530150109) do
t.boolean "system", default: false, null: false
t.text "st_diff"
t.integer "updated_by_id"
t.boolean "is_award", default: false, null: false
t.string "type"
end
......@@ -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", ["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", ["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", ["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
......
......@@ -278,6 +278,30 @@ Response:
[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 single build of a project
......
......@@ -29,7 +29,7 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps
end
step 'I click link "bug"' do
page.find('.js-label-select').click
page.find('.js-label-select', visible: true).click
sleep 0.5
execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
end
......
......@@ -191,15 +191,15 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'issue "Release 0.4" have 2 upvotes and 1 downvote' do
issue = Issue.find_by(title: 'Release 0.4')
create_list(:upvote_note, 2, project: project, noteable: issue)
create(:downvote_note, project: project, noteable: issue)
awardable = Issue.find_by(title: 'Release 0.4')
create_list(:award_emoji, 2, awardable: awardable)
create(:award_emoji, :downvote, awardable: awardable)
end
step 'issue "Tweet control" have 1 upvote and 2 downvotes' do
issue = Issue.find_by(title: 'Tweet control')
create(:upvote_note, project: project, noteable: issue)
create_list(:downvote_note, 2, project: project, noteable: issue)
awardable = Issue.find_by(title: 'Tweet control')
create(:award_emoji, :upvote, awardable: awardable)
create_list(:award_emoji, 2, awardable: awardable, name: 'thumbsdown')
end
step 'The list should be sorted by "Least popular"' do
......
......@@ -179,14 +179,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'merge request "Bug NS-04" have 2 upvotes and 1 downvote' do
merge_request = MergeRequest.find_by(title: 'Bug NS-04')
create_list(:upvote_note, 2, project: project, noteable: merge_request)
create(:downvote_note, project: project, noteable: merge_request)
create_list(:award_emoji, 2, awardable: merge_request)
create(:award_emoji, :downvote, awardable: merge_request)
end
step 'merge request "Bug NS-06" have 1 upvote and 2 downvotes' do
merge_request = MergeRequest.find_by(title: 'Bug NS-06')
create(:upvote_note, project: project, noteable: merge_request)
create_list(:downvote_note, 2, project: project, noteable: merge_request)
awardable = MergeRequest.find_by(title: 'Bug NS-06')
create(:award_emoji, awardable: awardable)
create_list(:award_emoji, 2, :downvote, awardable: awardable)
end
step 'The list should be sorted by "Least popular"' do
......
......@@ -171,15 +171,17 @@ module API
expose :label_names, as: :labels
expose :milestone, using: Entities::Milestone
expose :assignee, :author, using: Entities::UserBasic
expose :subscribed do |issue, options|
issue.subscribed?(options[:current_user])
end
expose :user_notes_count
expose :upvotes, :downvotes
end
class MergeRequest < ProjectEntity
expose :target_branch, :source_branch
expose :upvotes, :downvotes
expose :upvotes, :downvotes
expose :author, :assignee, using: Entities::UserBasic
expose :source_project_id, :target_project_id
expose :label_names, as: :labels
......@@ -217,8 +219,8 @@ module API
expose :system?, as: :system
expose :noteable_id, :noteable_type
# upvote? and downvote? are deprecated, always return false
expose :upvote?, as: :upvote
expose :downvote?, as: :downvote
expose(:upvote?) { |note| false }
expose(:downvote?) { |note| false }
end
class MRNote < Grape::Entity
......
......@@ -57,7 +57,7 @@ module API
not_found! "File" unless blob
content_type 'text/plain'
header *Gitlab::Workhorse.send_git_blob(repo, blob)
header(*Gitlab::Workhorse.send_git_blob(repo, blob))
end
# Get a raw blob contents by blob sha
......@@ -83,7 +83,7 @@ module API
env['api.format'] = :txt
content_type blob.mime_type
header *Gitlab::Workhorse.send_git_blob(repo, blob)
header(*Gitlab::Workhorse.send_git_blob(repo, blob))
end
# Get a an archive of the repository
......@@ -98,7 +98,7 @@ module API
authorize! :download_code, user_project
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
not_found!('File')
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
def report_success(success)
if success
$progress.puts '[DONE]'.green
$progress.puts '[DONE]'.color(:green)
else
$progress.puts '[FAILED]'.red
$progress.puts '[FAILED]'.color(:red)
end
end
end
......
......@@ -27,9 +27,9 @@ module Backup
# Set file permissions on open to prevent chmod races.
tar_system_options = {out: [tar_file, 'w', Gitlab.config.backup.archive_permissions]}
if Kernel.system('tar', '-cf', '-', *backup_contents, tar_system_options)
$progress.puts "done".green
$progress.puts "done".color(:green)
else
puts "creating archive #{tar_file} failed".red
puts "creating archive #{tar_file} failed".color(:red)
abort 'Backup failed'
end
......@@ -43,7 +43,7 @@ module Backup
connection_settings = Gitlab.config.backup.upload.connection
if connection_settings.blank?
$progress.puts "skipped".yellow
$progress.puts "skipped".color(:yellow)
return
end
......@@ -53,9 +53,9 @@ module Backup
if directory.files.create(key: tar_file, body: File.open(tar_file), public: false,
multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
encryption: Gitlab.config.backup.upload.encryption)
$progress.puts "done".green
$progress.puts "done".color(:green)
else
puts "uploading backup to #{remote_directory} failed".red
puts "uploading backup to #{remote_directory} failed".color(:red)
abort 'Backup failed'
end
end
......@@ -67,9 +67,9 @@ module Backup
next unless File.exist?(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
puts "deleting tmp directory '#{dir}' failed".red
puts "deleting tmp directory '#{dir}' failed".color(:red)
abort 'Backup failed'
end
end
......@@ -95,9 +95,9 @@ module Backup
end
end
$progress.puts "done. (#{removed} removed)".green
$progress.puts "done. (#{removed} removed)".color(:green)
else
$progress.puts "skipping".yellow
$progress.puts "skipping".color(:yellow)
end
end
......@@ -124,20 +124,20 @@ module Backup
$progress.print "Unpacking backup ... "
unless Kernel.system(*%W(tar -xf #{tar_file}))
puts "unpacking backup failed".red
puts "unpacking backup failed".color(:red)
exit 1
else
$progress.puts "done".green
$progress.puts "done".color(:green)
end
ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0
# restoring mismatching backups can lead to unexpected problems
if settings[:gitlab_version] != Gitlab::VERSION
puts "GitLab version mismatch:".red
puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".red
puts " Please switch to the following version and try again:".red
puts " version: #{settings[:gitlab_version]}".red
puts "GitLab version mismatch:".color(: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:".color(:red)
puts " version: #{settings[:gitlab_version]}".color(:red)
puts
puts "Hint: git checkout v#{settings[:gitlab_version]}"
exit 1
......
......@@ -14,14 +14,14 @@ module Backup
FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.path)) if project.namespace
if project.empty_repo?
$progress.puts "[SKIPPED]".cyan
$progress.puts "[SKIPPED]".color(:cyan)
else
cmd = %W(tar -cf #{path_to_bundle(project)} -C #{path_to_repo(project)} .)
output, status = Gitlab::Popen.popen(cmd)
if status.zero?
$progress.puts "[DONE]".green
$progress.puts "[DONE]".color(:green)
else
puts "[FAILED]".red
puts "[FAILED]".color(:red)
puts "failed: #{cmd.join(' ')}"
puts output
abort 'Backup failed'
......@@ -33,14 +33,14 @@ module Backup
if File.exists?(path_to_repo(wiki))
$progress.print " * #{wiki.path_with_namespace} ... "
if wiki.repository.empty?
$progress.puts " [SKIPPED]".cyan
$progress.puts " [SKIPPED]".color(:cyan)
else
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)
if status.zero?
$progress.puts " [DONE]".green
$progress.puts " [DONE]".color(:green)
else
puts " [FAILED]".red
puts " [FAILED]".color(:red)
puts "failed: #{cmd.join(' ')}"
abort 'Backup failed'
end
......@@ -71,9 +71,9 @@ module Backup
end
if system(*cmd, silent)
$progress.puts "[DONE]".green
$progress.puts "[DONE]".color(:green)
else
puts "[FAILED]".red
puts "[FAILED]".color(:red)
puts "failed: #{cmd.join(' ')}"
abort 'Restore failed'
end
......@@ -90,21 +90,21 @@ module Backup
cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)})
if system(*cmd, silent)
$progress.puts " [DONE]".green
$progress.puts " [DONE]".color(:green)
else
puts " [FAILED]".red
puts " [FAILED]".color(:red)
puts "failed: #{cmd.join(' ')}"
abort 'Restore failed'
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"
if system(cmd)
$progress.puts " [DONE]".green
$progress.puts " [DONE]".color(:green)
else
puts " [FAILED]".red
puts " [FAILED]".color(:red)
puts "failed: #{cmd}"
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
file.rewind
cmd = []
cmd.push *%W(ssh-keygen)
cmd.push *%W(-E md5) if explicit_fingerprint_algorithm?
cmd.push *%W(-lf #{file.path})
cmd.push('ssh-keygen')
cmd.push('-E', 'md5') if explicit_fingerprint_algorithm?
cmd.push('-lf', file.path)
cmd_output, cmd_status = popen(cmd, '/tmp')
end
......
......@@ -93,6 +93,7 @@ module Gitlab
end
protected
def base_config
Gitlab.config.ldap
end
......
......@@ -5,7 +5,7 @@ module Gitlab
SeedFu.quiet = true
yield
SeedFu.quiet = false
puts "\nOK".green
puts "\nOK".color(:green)
end
def self.by_user(user)
......
......@@ -40,14 +40,14 @@ namespace :gitlab do
removed.
MSG
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)
end
# Drop all tables Load the schema to ensure we don't have any newer tables
# 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
$progress.puts 'done'.green
$progress.puts 'done'.color(:green)
Rake::Task['gitlab:backup:db:restore'].invoke
end
Rake::Task['gitlab:backup:repo:restore'].invoke unless backup.skipped?('repositories')
......@@ -63,141 +63,141 @@ namespace :gitlab do
namespace :repo do
task create: :environment do
$progress.puts "Dumping repositories ...".blue
$progress.puts "Dumping repositories ...".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("repositories")
$progress.puts "[SKIPPED]".cyan
$progress.puts "[SKIPPED]".color(:cyan)
else
Backup::Repository.new.dump
$progress.puts "done".green
$progress.puts "done".color(:green)
end
end
task restore: :environment do
$progress.puts "Restoring repositories ...".blue
$progress.puts "Restoring repositories ...".color(:blue)
Backup::Repository.new.restore
$progress.puts "done".green
$progress.puts "done".color(:green)
end
end
namespace :db do
task create: :environment do
$progress.puts "Dumping database ... ".blue
$progress.puts "Dumping database ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("db")
$progress.puts "[SKIPPED]".cyan
$progress.puts "[SKIPPED]".color(:cyan)
else
Backup::Database.new.dump
$progress.puts "done".green
$progress.puts "done".color(:green)
end
end
task restore: :environment do
$progress.puts "Restoring database ... ".blue
$progress.puts "Restoring database ... ".color(:blue)
Backup::Database.new.restore
$progress.puts "done".green
$progress.puts "done".color(:green)
end
end
namespace :builds do
task create: :environment do
$progress.puts "Dumping builds ... ".blue
$progress.puts "Dumping builds ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("builds")
$progress.puts "[SKIPPED]".cyan
$progress.puts "[SKIPPED]".color(:cyan)
else
Backup::Builds.new.dump
$progress.puts "done".green
$progress.puts "done".color(:green)
end
end
task restore: :environment do
$progress.puts "Restoring builds ... ".blue
$progress.puts "Restoring builds ... ".color(:blue)
Backup::Builds.new.restore
$progress.puts "done".green
$progress.puts "done".color(:green)
end
end
namespace :uploads do
task create: :environment do
$progress.puts "Dumping uploads ... ".blue
$progress.puts "Dumping uploads ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("uploads")
$progress.puts "[SKIPPED]".cyan
$progress.puts "[SKIPPED]".color(:cyan)
else
Backup::Uploads.new.dump
$progress.puts "done".green
$progress.puts "done".color(:green)
end
end
task restore: :environment do
$progress.puts "Restoring uploads ... ".blue
$progress.puts "Restoring uploads ... ".color(:blue)
Backup::Uploads.new.restore
$progress.puts "done".green
$progress.puts "done".color(:green)
end
end
namespace :artifacts do
task create: :environment do
$progress.puts "Dumping artifacts ... ".blue
$progress.puts "Dumping artifacts ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("artifacts")
$progress.puts "[SKIPPED]".cyan
$progress.puts "[SKIPPED]".color(:cyan)
else
Backup::Artifacts.new.dump
$progress.puts "done".green
$progress.puts "done".color(:green)
end
end
task restore: :environment do
$progress.puts "Restoring artifacts ... ".blue
$progress.puts "Restoring artifacts ... ".color(:blue)
Backup::Artifacts.new.restore
$progress.puts "done".green
$progress.puts "done".color(:green)
end
end
namespace :lfs 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")
$progress.puts "[SKIPPED]".cyan
$progress.puts "[SKIPPED]".color(:cyan)
else
Backup::Lfs.new.dump
$progress.puts "done".green
$progress.puts "done".color(:green)
end
end
task restore: :environment do
$progress.puts "Restoring lfs objects ... ".blue
$progress.puts "Restoring lfs objects ... ".color(:blue)
Backup::Lfs.new.restore
$progress.puts "done".green
$progress.puts "done".color(:green)
end
end
namespace :registry 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 ENV["SKIP"] && ENV["SKIP"].include?("registry")
$progress.puts "[SKIPPED]".cyan
$progress.puts "[SKIPPED]".color(:cyan)
else
Backup::Registry.new.dump
$progress.puts "done".green
$progress.puts "done".color(:green)
end
else
$progress.puts "[DISABLED]".cyan
$progress.puts "[DISABLED]".color(:cyan)
end
end
task restore: :environment do
$progress.puts "Restoring container registry images ... ".blue
$progress.puts "Restoring container registry images ... ".color(:blue)
if Gitlab.config.registry.enabled
Backup::Registry.new.restore
$progress.puts "done".green
$progress.puts "done".color(:green)
else
$progress.puts "[DISABLED]".cyan
$progress.puts "[DISABLED]".color(:cyan)
end
end
end
......
This diff is collapsed.
......@@ -10,7 +10,7 @@ namespace :gitlab do
git_base_path = Gitlab.config.gitlab_shell.repos_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... "
all_dirs.reject! do |dir|
......@@ -29,17 +29,17 @@ namespace :gitlab do
if remove_flag
if FileUtils.rm_rf dir_path
puts "Removed...#{dir_path}".red
puts "Removed...#{dir_path}".color(:red)
else
puts "Cannot remove #{dir_path}".red
puts "Cannot remove #{dir_path}".color(:red)
end
else
puts "Can be removed: #{dir_path}".red
puts "Can be removed: #{dir_path}".color(:red)
end
end
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
......@@ -75,19 +75,19 @@ namespace :gitlab do
next unless user.ldap_user?
print "#{user.name} (#{user.ldap_identity.extern_uid}) ..."
if Gitlab::LDAP::Access.allowed?(user)
puts " [OK]".green
puts " [OK]".color(:green)
else
if block_flag
user.block! unless user.blocked?
puts " [BLOCKED]".red
puts " [BLOCKED]".color(:red)
else
puts " [NOT IN LDAP]".yellow
puts " [NOT IN LDAP]".color(:yellow)
end
end
end
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
......
......@@ -3,22 +3,22 @@ namespace :gitlab do
desc 'GitLab | Manually insert schema migration version'
task :mark_migration_complete, [:version] => :environment do |_, args|
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
end
version = args[:version].to_i
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
end
sql = "INSERT INTO schema_migrations (version) VALUES (#{version})"
begin
ActiveRecord::Base.connection.execute(sql)
puts "Successfully marked '#{version}' as complete".green
puts "Successfully marked '#{version}' as complete".color(:green)
rescue ActiveRecord::RecordNotUnique
puts "Migration version '#{version}' is already marked complete".yellow
puts "Migration version '#{version}' is already marked complete".color(:yellow)
end
end
......
......@@ -5,7 +5,7 @@ namespace :gitlab do
task repack: :environment do
failures = perform_git_cmd(%W(git repack -a --quiet), "Repacking repo")
if failures.empty?
puts "Done".green
puts "Done".color(:green)
else
output_failures(failures)
end
......@@ -15,7 +15,7 @@ namespace :gitlab do
task gc: :environment do
failures = perform_git_cmd(%W(git gc --auto --quiet), "Garbage Collecting")
if failures.empty?
puts "Done".green
puts "Done".color(:green)
else
output_failures(failures)
end
......@@ -25,7 +25,7 @@ namespace :gitlab do
task prune: :environment do
failures = perform_git_cmd(%W(git prune), "Git Prune")
if failures.empty?
puts "Done".green
puts "Done".color(:green)
else
output_failures(failures)
end
......@@ -47,7 +47,7 @@ namespace :gitlab do
end
def output_failures(failures)
puts "The following repositories reported errors:".red
puts "The following repositories reported errors:".color(:red)
failures.each { |f| puts "- #{f}" }
end
......
......@@ -23,7 +23,7 @@ namespace :gitlab do
group_name, name = File.split(path)
group_name = nil if group_name == '.'
puts "Processing #{repo_path}".yellow
puts "Processing #{repo_path}".color(:yellow)
if path.end_with?('.wiki')
puts " * Skipping wiki repo"
......@@ -51,9 +51,9 @@ namespace :gitlab do
group.path = group_name
group.owner = user
if group.save
puts " * Created Group #{group.name} (#{group.id})".green
puts " * Created Group #{group.name} (#{group.id})".color(:green)
else
puts " * Failed trying to create group #{group.name}".red
puts " * Failed trying to create group #{group.name}".color(:red)
end
end
# set project group
......@@ -63,17 +63,17 @@ namespace :gitlab do
project = Projects::CreateService.new(user, project_params).execute
if project.persisted?
puts " * Created #{project.name} (#{repo_path})".green
puts " * Created #{project.name} (#{repo_path})".color(:green)
project.update_repository_size
project.update_commit_count
else
puts " * Failed trying to create #{project.name} (#{repo_path})".red
puts " Errors: #{project.errors.messages}".red
puts " * Failed trying to create #{project.name} (#{repo_path})".color(:red)
puts " Errors: #{project.errors.messages}".color(:red)
end
end
end
puts "Done!".green
puts "Done!".color(:green)
end
end
end
......@@ -15,15 +15,15 @@ namespace :gitlab do
rake_version = run_and_match(%W(rake --version), /[\d\.]+/).try(:to_s)
puts ""
puts "System information".yellow
puts "System:\t\t#{os_name || "unknown".red}"
puts "System information".color(:yellow)
puts "System:\t\t#{os_name || "unknown".color(:red)}"
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 "Ruby Version:\t#{ruby_version || "unknown".red}"
puts "Gem Version:\t#{gem_version || "unknown".red}"
puts "Bundler Version:#{bunder_version || "unknown".red}"
puts "Rake Version:\t#{rake_version || "unknown".red}"
puts "Ruby Version:\t#{ruby_version || "unknown".color(:red)}"
puts "Gem Version:\t#{gem_version || "unknown".color(:red)}"
puts "Bundler Version:#{bunder_version || "unknown".color(:red)}"
puts "Rake Version:\t#{rake_version || "unknown".color(:red)}"
puts "Sidekiq Version:#{Sidekiq::VERSION}"
......@@ -39,7 +39,7 @@ namespace :gitlab do
omniauth_providers.map! { |provider| provider['name'] }
puts ""
puts "GitLab information".yellow
puts "GitLab information".color(:yellow)
puts "Version:\t#{Gitlab::VERSION}"
puts "Revision:\t#{Gitlab::REVISION}"
puts "Directory:\t#{Rails.root}"
......@@ -47,9 +47,9 @@ namespace :gitlab do
puts "URL:\t\t#{Gitlab.config.gitlab.url}"
puts "HTTP Clone URL:\t#{http_clone_url}"
puts "SSH Clone URL:\t#{ssh_clone_url}"
puts "Using LDAP:\t#{Gitlab.config.ldap.enabled ? "yes".green : "no"}"
puts "Using Omniauth:\t#{Gitlab.config.omniauth.enabled ? "yes".green : "no"}"
puts "Omniauth Providers: #{omniauth_providers.map(&:magenta).join(', ')}" if Gitlab.config.omniauth.enabled
puts "Using LDAP:\t#{Gitlab.config.ldap.enabled ? "yes".color(:green) : "no"}"
puts "Using Omniauth:\t#{Gitlab.config.omniauth.enabled ? "yes".color(:green) : "no"}"
puts "Omniauth Providers: #{omniauth_providers.join(', ')}" if Gitlab.config.omniauth.enabled
......@@ -60,8 +60,8 @@ namespace :gitlab do
end
puts ""
puts "GitLab Shell".yellow
puts "Version:\t#{gitlab_shell_version || "unknown".red}"
puts "GitLab Shell".color(:yellow)
puts "Version:\t#{gitlab_shell_version || "unknown".color(:red)}"
puts "Repositories:\t#{Gitlab.config.gitlab_shell.repos_path}"
puts "Hooks:\t\t#{Gitlab.config.gitlab_shell.hooks_path}"
puts "Git:\t\t#{Gitlab.config.git.bin_path}"
......
......@@ -118,12 +118,12 @@ namespace :gitlab do
puts ""
unless $?.success?
puts "Failed to add keys...".red
puts "Failed to add keys...".color(:red)
exit 1
end
rescue Gitlab::TaskAbortedByUserError
puts "Quitting...".red
puts "Quitting...".color(:red)
exit 1
end
......
......@@ -2,7 +2,7 @@ module Gitlab
class TaskAbortedByUserError < StandardError; end
end
String.disable_colorization = true unless STDOUT.isatty
require 'rainbow/ext/string'
# Prevent StateMachine warnings from outputting during a cron task
StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON']
......@@ -14,7 +14,7 @@ namespace :gitlab do
# Returns "yes" the user chose to continue
# Raises Gitlab::TaskAbortedByUserError if the user chose *not* 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"
end
......@@ -98,10 +98,10 @@ namespace :gitlab do
gitlab_user = Gitlab.config.gitlab.user
current_user = run(%W(whoami)).chomp
unless current_user == gitlab_user
puts " Warning ".colorize(:black).on_yellow
puts " You are running as user #{current_user.magenta}, we hope you know what you are doing."
puts " Warning ".color(:black).background(:yellow)
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 " 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 ""
end
@warned_user_not_gitlab = true
......
......@@ -6,17 +6,17 @@ namespace :gitlab do
count = scope.count
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
ask_to_continue
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
puts "Quitting...".red
puts "Quitting...".color(:red)
end
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
......
......@@ -6,15 +6,15 @@ namespace :gitlab do
ask_to_continue unless ENV['force'] == 'yes'
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?
puts "skipping, because the repo is empty".magenta
puts "skipping, because the repo is empty".color(:magenta)
next
end
project.update_commit_count
puts project.commit_count.to_s.green
puts project.commit_count.to_s.color(:green)
end
end
end
......@@ -2,14 +2,14 @@ namespace :gitlab do
desc "GitLab | Update gitignore"
task :update_gitignore do
unless clone_gitignores
puts "Cloning the gitignores failed".red
puts "Cloning the gitignores failed".color(:red)
return
end
remove_unneeded_files(gitignore_directory)
remove_unneeded_files(global_directory)
puts "Done".green
puts "Done".color(:green)
end
def clone_gitignores
......
......@@ -12,9 +12,9 @@ namespace :gitlab do
print "- #{project.name} ... "
web_hook = project.hooks.new(url: web_hook_url)
if web_hook.save
puts "added".green
puts "added".color(:green)
else
print "failed".red
print "failed".color(:red)
puts " [#{web_hook.errors.full_messages.to_sentence}]"
end
end
......@@ -57,7 +57,7 @@ namespace :gitlab do
if namespace
Project.in_namespace(namespace.id)
else
puts "Namespace not found: #{namespace_path}".red
puts "Namespace not found: #{namespace_path}".color(:red)
exit 2
end
end
......
desc "GitLab | Build internal ids for issues and merge requests"
task migrate_iids: :environment do
puts 'Issues'.yellow
puts 'Issues'.color(:yellow)
Issue.where(iid: nil).find_each(batch_size: 100) do |issue|
begin
issue.set_iid
......@@ -15,7 +15,7 @@ task migrate_iids: :environment do
end
puts 'done'
puts 'Merge Requests'.yellow
puts 'Merge Requests'.color(:yellow)
MergeRequest.where(iid: nil).find_each(batch_size: 100) do |mr|
begin
mr.set_iid
......@@ -30,7 +30,7 @@ task migrate_iids: :environment do
end
puts 'done'
puts 'Milestones'.yellow
puts 'Milestones'.color(:yellow)
Milestone.where(iid: nil).find_each(batch_size: 100) do |m|
begin
m.set_iid
......
......@@ -52,7 +52,7 @@ def run_spinach_tests(tags)
tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp)
puts ''
puts "Spinach tests for #{tags}: Retrying tests... #{tests}".red
puts "Spinach tests for #{tags}: Retrying tests... #{tests}".color(:red)
puts ''
sleep(3)
success = run_spinach_command(tests)
......
......@@ -31,9 +31,9 @@ describe GroupsController do
let(:issue_2) { create(:issue, project: project) }
before do
create_list(:upvote_note, 3, project: project, noteable: issue_2)
create_list(:upvote_note, 2, project: project, noteable: issue_1)
create_list(:downvote_note, 2, project: project, noteable: issue_2)
create_list(:award_emoji, 3, awardable: issue_2)
create_list(:award_emoji, 2, awardable: issue_1)
create_list(:award_emoji, 2, :downvote, awardable: issue_2,)
sign_in(user)
end
......@@ -56,9 +56,9 @@ describe GroupsController do
let(:merge_request_2) { create(:merge_request, :simple, source_project: project) }
before do
create_list(:upvote_note, 3, project: project, noteable: merge_request_2)
create_list(:upvote_note, 2, project: project, noteable: merge_request_1)
create_list(:downvote_note, 2, project: project, noteable: merge_request_2)
create_list(:award_emoji, 3, awardable: merge_request_2)
create_list(:award_emoji, 2, awardable: merge_request_1)
create_list(:award_emoji, 2, :downvote, awardable: merge_request_2)
sign_in(user)
end
......
......@@ -250,4 +250,20 @@ describe Projects::IssuesController do
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
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment