summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlfredo Sumaran <alfredo@gitlab.com>2016-04-06 16:35:04 -0500
committerAlfredo Sumaran <alfredo@gitlab.com>2016-04-06 16:35:04 -0500
commitbfcee4a51d8cd7cecd7a63e52f13979a436a458e (patch)
tree9905169af129a07a924286e2a7d389a00e912adb
parent946b4519c99a91c18a23041c090568231410785a (diff)
parentb6d5fcd4775718676f1a11ddb6a88a08c67c9d0a (diff)
downloadgitlab-ce-bfcee4a51d8cd7cecd7a63e52f13979a436a458e.tar.gz
Merge branch 'master' into issue_14952
-rw-r--r--.gitlab-ci.yml76
-rw-r--r--CHANGELOG2
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee11
-rw-r--r--app/assets/javascripts/issues.js.coffee15
-rw-r--r--app/assets/javascripts/lib/url_utility.js.coffee31
-rw-r--r--app/assets/javascripts/search_autocomplete.js.coffee58
-rw-r--r--app/assets/stylesheets/framework/variables.scss20
-rw-r--r--app/assets/stylesheets/pages/projects.scss4
-rw-r--r--app/assets/stylesheets/pages/search.scss20
-rw-r--r--app/assets/stylesheets/pages/status.scss2
-rw-r--r--app/models/project_services/builds_email_service.rb11
-rw-r--r--app/views/layouts/_search.html.haml4
-rw-r--r--app/views/projects/notes/_note.html.haml2
-rw-r--r--doc/api/users.md6
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/instrumentation.md37
-rw-r--r--lib/api/entities.rb2
-rw-r--r--lib/api/users.rb6
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb2
-rw-r--r--lib/gitlab/metrics.rb26
-rw-r--r--spec/features/issues/filter_issues_spec.rb119
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb8
-rw-r--r--spec/lib/gitlab/metrics_spec.rb47
-rw-r--r--spec/models/project_services/builds_email_service_spec.rb24
24 files changed, 399 insertions, 135 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 336ceb31021..1dc49ca336d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,7 +2,6 @@ image: "ruby:2.1"
services:
- mysql:latest
- - postgres:latest
- redis:latest
cache:
@@ -35,123 +34,79 @@ spec:feature:
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
- tags:
- - ruby
- - mysql
spec:api:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api
- tags:
- - ruby
- - mysql
spec:models:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models
- tags:
- - ruby
- - mysql
spec:lib:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib
- tags:
- - ruby
- - mysql
spec:services:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services
- tags:
- - ruby
- - mysql
spec:other:
stage: test
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
- tags:
- - ruby
- - mysql
spinach:project:half:
stage: test
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half
- tags:
- - ruby
- - mysql
spinach:project:rest:
stage: test
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest
- tags:
- - ruby
- - mysql
spinach:other:
stage: test
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other
- tags:
- - ruby
- - mysql
teaspoon:
stage: test
script:
- RAILS_ENV=test bundle exec teaspoon
- tags:
- - ruby
- - mysql
rubocop:
stage: test
script:
- bundle exec rubocop
- tags:
- - ruby
- - mysql
scss-lint:
stage: test
script:
- bundle exec rake scss_lint
- tags:
- - ruby
brakeman:
stage: test
script:
- bundle exec rake brakeman
- tags:
- - ruby
- - mysql
flog:
stage: test
script:
- bundle exec rake flog
- tags:
- - ruby
- - mysql
flay:
stage: test
script:
- bundle exec rake flay
- tags:
- - ruby
- - mysql
bundler:audit:
stage: test
@@ -159,9 +114,6 @@ bundler:audit:
- master
script:
- "bundle exec bundle-audit check --update --ignore OSVDB-115941"
- tags:
- - ruby
- - mysql
# Ruby 2.2 jobs
@@ -177,9 +129,6 @@ spec:feature:ruby22:
key: "ruby22"
paths:
- vendor
- tags:
- - ruby
- - mysql
spec:api:ruby22:
stage: test
@@ -192,9 +141,6 @@ spec:api:ruby22:
key: "ruby22"
paths:
- vendor
- tags:
- - ruby
- - mysql
spec:models:ruby22:
stage: test
@@ -207,9 +153,6 @@ spec:models:ruby22:
key: "ruby22"
paths:
- vendor
- tags:
- - ruby
- - mysql
spec:lib:ruby22:
stage: test
@@ -222,9 +165,6 @@ spec:lib:ruby22:
key: "ruby22"
paths:
- vendor
- tags:
- - ruby
- - mysql
spec:services:ruby22:
stage: test
@@ -237,9 +177,6 @@ spec:services:ruby22:
key: "ruby22"
paths:
- vendor
- tags:
- - ruby
- - mysql
spec:other:ruby22:
stage: test
@@ -252,9 +189,6 @@ spec:other:ruby22:
key: "ruby22"
paths:
- vendor
- tags:
- - ruby
- - mysql
spinach:project:half:ruby22:
stage: test
@@ -268,9 +202,6 @@ spinach:project:half:ruby22:
key: "ruby22"
paths:
- vendor
- tags:
- - ruby
- - mysql
spinach:project:rest:ruby22:
stage: test
@@ -284,9 +215,6 @@ spinach:project:rest:ruby22:
key: "ruby22"
paths:
- vendor
- tags:
- - ruby
- - mysql
spinach:other:ruby22:
stage: test
@@ -300,10 +228,6 @@ spinach:other:ruby22:
key: "ruby22"
paths:
- vendor
- tags:
- - ruby
- - mysql
-
notify:slack:
stage: notifications
diff --git a/CHANGELOG b/CHANGELOG
index 8db9a9b0d1e..92a0d8547e4 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -14,6 +14,7 @@ v 8.7.0 (unreleased)
- Add links to CI setup documentation from project settings and builds pages
- Handle nil descriptions in Slack issue messages (Stan Hu)
- Add default scope to projects to exclude projects pending deletion
+ - Ensure empty recipients are rejected in BuildsEmailService
- Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
- Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
- Gracefully handle notes on deleted commits in merge requests (Stan Hu)
@@ -22,6 +23,7 @@ v 8.7.0 (unreleased)
- Remove "Congratulations!" tweet button on newly-created project. (Connor Shea)
- Improved UX of the navigation sidebar
- Build status notifications
+ - API: Expose user location (Robert Schilling)
v 8.6.5 (unreleased)
- Check permissions when user attempts to import members from another project
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index 2a4811b8763..e8d25591f63 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -177,10 +177,11 @@ class GitLabDropdown
selector = ".dropdown-page-one .dropdown-content a"
@dropdown.on "click", selector, (e) ->
- selected = self.rowClicked $(@)
+ $el = $(@)
+ selected = self.rowClicked $el
if self.options.clicked
- self.options.clicked(selected)
+ self.options.clicked(selected, $el, e)
# Finds an element inside wrapper element
getElement: (selector) ->
@@ -360,6 +361,8 @@ class GitLabDropdown
# Toggle the dropdown label
if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel
+ else
+ selectedObject
else
if !value?
field.remove()
@@ -375,7 +378,7 @@ class GitLabDropdown
if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject)
if value?
- if !field.length
+ if !field.length and fieldName
# Create hidden input for form
input = "<input type='hidden' name='#{fieldName}' value='#{value}' />"
if @options.inputId?
@@ -394,7 +397,7 @@ class GitLabDropdown
selector = ".dropdown-page-one #{selector}"
# simulate a click on the first link
- $(selector).trigger "click"
+ $(selector, @dropdown).trigger "click"
addArrowKeyEvent: ->
ARROW_KEY_CODES = [38, 40]
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee
index b1479bfb449..0d9f2094c2a 100644
--- a/app/assets/javascripts/issues.js.coffee
+++ b/app/assets/javascripts/issues.js.coffee
@@ -26,6 +26,20 @@
$(".selected_issue").bind "change", Issues.checkChanged
+ # Update state filters if present in page
+ updateStateFilters: ->
+ stateFilters = $('.issues-state-filters')
+ newParams = {}
+ paramKeys = ['author_id', 'label_name', 'milestone_title', 'assignee_id', 'issue_search']
+
+ for paramKey in paramKeys
+ newParams[paramKey] = gl.utils.getUrlParameter(paramKey) or ''
+
+ if stateFilters.length
+ stateFilters.find('a').each ->
+ initialUrl = $(this).attr 'href'
+ $(this).attr 'href', gl.utils.mergeUrlParams(newParams, initialUrl)
+
# Make sure we trigger ajax request only after user stop typing
initSearch: ->
@timer = null
@@ -54,6 +68,7 @@
# Change url so if user reload a page - search results are saved
history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issues.reload()
+ Issues.updateStateFilters()
dataType: "json"
checkChanged: ->
diff --git a/app/assets/javascripts/lib/url_utility.js.coffee b/app/assets/javascripts/lib/url_utility.js.coffee
new file mode 100644
index 00000000000..abd556e0b4e
--- /dev/null
+++ b/app/assets/javascripts/lib/url_utility.js.coffee
@@ -0,0 +1,31 @@
+((w) ->
+
+ w.gl ?= {}
+ w.gl.utils ?= {}
+
+ w.gl.utils.getUrlParameter = (sParam) ->
+ sPageURL = decodeURIComponent(window.location.search.substring(1))
+ sURLVariables = sPageURL.split('&')
+ sParameterName = undefined
+ i = 0
+ while i < sURLVariables.length
+ sParameterName = sURLVariables[i].split('=')
+ if sParameterName[0] is sParam
+ return if sParameterName[1] is undefined then true else sParameterName[1]
+ i++
+
+ # #
+ # @param {Object} params - url keys and value to merge
+ # @param {String} url
+ # #
+ w.gl.utils.mergeUrlParams = (params, url) ->
+ newUrl = decodeURIComponent(url)
+ for paramName, paramValue of params
+ pattern = new RegExp "\\b(#{paramName}=).*?(&|$)"
+ if url.search(pattern) >= 0
+ newUrl = newUrl.replace pattern, "$1#{paramValue}$2"
+ else
+ newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}"
+ newUrl
+
+) window
diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee
index 030655491bf..6a7b4ad1db7 100644
--- a/app/assets/javascripts/search_autocomplete.js.coffee
+++ b/app/assets/javascripts/search_autocomplete.js.coffee
@@ -62,6 +62,8 @@ class @SearchAutocomplete
search:
fields: ['text']
data: @getData.bind(@)
+ selectable: true
+ clicked: @onClick.bind(@)
getData: (term, callback) ->
_this = @
@@ -102,6 +104,8 @@ class @SearchAutocomplete
lastCategory = suggestion.category
data.push
+ id: "#{suggestion.category.toLowerCase()}-#{suggestion.id}"
+ category: suggestion.category
text: suggestion.label
url: suggestion.url
@@ -133,12 +137,19 @@ class @SearchAutocomplete
}
bindEvents: ->
+ $(document).on 'click', @onDocumentClick
@searchInput.on 'keydown', @onSearchInputKeyDown
@searchInput.on 'keyup', @onSearchInputKeyUp
@searchInput.on 'click', @onSearchInputClick
@searchInput.on 'focus', @onSearchInputFocus
- @searchInput.on 'blur', @onSearchInputBlur
- @clearInput.on 'click', @onRemoveLocationClick
+ @clearInput.on 'click', @onClearInputClick
+
+ onDocumentClick: (e) =>
+ # If clicking outside the search box
+ # And search input is not focused
+ # And we are not clicking inside a suggestion
+ if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).parents('ul').length
+ @onSearchInputBlur()
enableAutocomplete: ->
# No need to enable anything if user is not logged in
@@ -181,6 +192,8 @@ class @SearchAutocomplete
# We should display the menu only when input is not empty
@enableAutocomplete()
+ @wrap.toggleClass 'has-value', !!e.target.value
+
# Avoid falsy value to be returned
return
@@ -189,27 +202,20 @@ class @SearchAutocomplete
e.stopImmediatePropagation()
onSearchInputFocus: =>
+ @isFocused = true
@wrap.addClass('search-active')
- onRemoveLocationClick: (e) =>
+ onClearInputClick: (e) =>
e.preventDefault()
- @removeLocationBadge()
@searchInput.val('').focus()
- @skipBlurEvent = true
onSearchInputBlur: (e) =>
- @skipBlurEvent = false
-
- # We should wait to make sure we are not clearing the input instead
- setTimeout( =>
- return if @skipBlurEvent
+ @isFocused = false
+ @wrap.removeClass('search-active')
- @wrap.removeClass('search-active')
-
- # If input is blank then restore state
- if @searchInput.val() is ''
- @restoreOriginalState()
- , 150)
+ # If input is blank then restore state
+ if @searchInput.val() is ''
+ @restoreOriginalState()
addLocationBadge: (item) ->
category = if item.category? then "#{item.category}: " else ''
@@ -268,3 +274,23 @@ class @SearchAutocomplete
<li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li>
</ul>"
@dropdownContent.html(html)
+
+ onClick: (item, $el, e) ->
+ if location.pathname.indexOf(item.url) isnt -1
+ e.preventDefault()
+ if not @badgePresent
+ if item.category is 'Projects'
+ @projectInputEl.val(item.id)
+ @addLocationBadge(
+ value: 'This project'
+ )
+
+ if item.category is 'Groups'
+ @groupInputEl.val(item.id)
+ @addLocationBadge(
+ value: 'This group'
+ )
+
+ $el.removeClass('is-active')
+ @disableAutocomplete()
+ @searchInput.val('').focus()
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index c2defd31884..8d3ad934a50 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -104,9 +104,9 @@ $orange-light: rgba(252, 109, 38, 0.80);
$orange-normal: #e75e40;
$orange-dark: #ce5237;
-$red-light: #f06559;
-$red-normal: #e52c5a;
-$red-dark: #d22852;
+$red-light: #e52c5a;
+$red-normal: #d22852;
+$red-dark: darken($red-normal, 5%);
$border-white-light: #f1f2f4;
$border-white-normal: #d6dae2;
@@ -128,9 +128,9 @@ $border-orange-light: #fc6d26;
$border-orange-normal: #ce5237;
$border-orange-dark: #c14e35;
-$border-red-light: #f24f41;
-$border-red-normal: #d22852;
-$border-red-dark: #ca264f;
+$border-red-light: #d22852;
+$border-red-normal: #ca264f;
+$border-red-dark: darken($border-red-normal, 5%);
$help-well-bg: #fafafa;
$help-well-border: #e5e5e5;
@@ -201,14 +201,14 @@ $award-emoji-new-btn-icon-color: #dcdcdc;
/*
* Search Box
*/
-$search-input-border-color: $dropdown-input-focus-border;
+$search-input-border-color: rgba(#4688f1, .8);
$search-input-focus-shadow-color: $dropdown-input-focus-shadow;
-$search-input-width: $dropdown-width;
+$search-input-width: 244px;
$location-badge-color: #aaa;
$location-badge-bg: $gray-normal;
+$location-badge-active-bg: #4f91f8;
$location-icon-color: #e7e9ed;
-$location-active-color: $gl-text-color;
-$location-active-bg: $search-input-border-color;
+$location-icon-active-color: #807e7e;
/*
* Notes
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 4e6aa8cd1a6..fcca9d4faf5 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -315,7 +315,7 @@ pre.light-well {
}
.git-empty {
- margin: 0 7px;
+ margin: 0 7px 7px;
h5 {
color: #5c5d5e;
@@ -401,7 +401,7 @@ pre.light-well {
}
.commit_short_id {
- margin-right: 5px;
+ margin: 0 5px;
color: $gl-link-color;
font-weight: 600;
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 3c74d25beb0..f0f3744c6fa 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -135,25 +135,25 @@
.location-badge {
@include transition(all .15s);
- background-color: $location-active-bg;
+ background-color: $location-badge-active-bg;
color: $white-light;
}
.search-input-wrap {
i {
- color: $location-active-color;
+ color: $location-icon-active-color;
}
}
+ }
- &.has-location-badge {
- .search-icon {
- display: none;
- }
+ &.has-value {
+ .search-icon {
+ display: none;
+ }
- .clear-icon {
- cursor: pointer;
- display: block;
- }
+ .clear-icon {
+ cursor: pointer;
+ display: block;
}
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 5e5e38a0ba6..dbb6daf0d70 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -1,4 +1,4 @@
-.container-fluid .content {
+.container-fluid {
.ci-status {
padding: 2px 7px;
margin-right: 5px;
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index f6313255cbb..f9f04838766 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -50,12 +50,15 @@ class BuildsEmailService < Service
def execute(push_data)
return unless supported_events.include?(push_data[:object_kind])
+ return unless should_build_be_notified?(push_data)
- if should_build_be_notified?(push_data)
+ recipients = all_recipients(push_data)
+
+ if recipients.any?
BuildEmailWorker.perform_async(
push_data[:build_id],
- all_recipients(push_data),
- push_data,
+ recipients,
+ push_data
)
end
end
@@ -84,7 +87,7 @@ class BuildsEmailService < Service
end
def all_recipients(data)
- all_recipients = recipients.split(',')
+ all_recipients = recipients.split(',').compact.reject(&:blank?)
if add_pusher? && data[:user][:email]
all_recipients << "#{data[:user][:email]}"
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index baa93ba4c1d..6b208c3d0bb 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -21,8 +21,8 @@
%a.is-focused.dropdown-menu-empty-link
Loading...
= dropdown_loading
- %i.search-icon
- %i.clear-icon.js-clear-input
+ %i.search-icon
+ %i.clear-icon.js-clear-input
= hidden_field_tag :group_id, @group.try(:id)
= hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id'
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 34fe1743f4b..a681d6dece4 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -18,7 +18,7 @@
= access
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil-square-o')
- = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
+ = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete' do
= icon('trash-o')
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text
diff --git a/doc/api/users.md b/doc/api/users.md
index 383e7c76ab0..7d2b4897cff 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -69,6 +69,7 @@ GET /users
"state": "blocked",
"created_at": "2012-05-23T08:01:01Z",
"bio": null,
+ "location": null,
"skype": "",
"linkedin": "",
"twitter": "",
@@ -126,6 +127,7 @@ Parameters:
"created_at": "2012-05-23T08:00:58Z",
"is_admin": false,
"bio": null,
+ "location": null,
"skype": "",
"linkedin": "",
"twitter": "",
@@ -154,6 +156,7 @@ Parameters:
"confirmed_at": "2012-05-23T08:00:58Z",
"last_sign_in_at": "2015-03-23T08:00:58Z",
"bio": null,
+ "location": null,
"skype": "",
"linkedin": "",
"twitter": "",
@@ -191,6 +194,7 @@ Parameters:
- `extern_uid` (optional) - External UID
- `provider` (optional) - External provider name
- `bio` (optional) - User's biography
+- `location` (optional) - User's location
- `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false
- `confirm` (optional) - Require confirmation - true (default) or false
@@ -218,6 +222,7 @@ Parameters:
- `extern_uid` - External UID
- `provider` - External provider name
- `bio` - User's biography
+- `location` (optional) - User's location
- `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false
- `external` (optional) - Flags the user as external - true or false(default)
@@ -260,6 +265,7 @@ GET /user
"state": "active",
"created_at": "2012-05-23T08:00:58Z",
"bio": null,
+ "location": null,
"skype": "",
"linkedin": "",
"twitter": "",
diff --git a/doc/development/README.md b/doc/development/README.md
index 1b281809afc..8940b558fb6 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -4,6 +4,7 @@
- [CI setup](ci_setup.md) for testing GitLab
- [Gotchas](gotchas.md) to avoid
- [How to dump production data to staging](db_dump.md)
+- [Instrumentation](instrumentation.md)
- [Migration Style Guide](migration_style_guide.md) for creating safe migrations
- [Rake tasks](rake_tasks.md) for development
- [Shell commands](shell_commands.md) in the GitLab codebase
diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md
new file mode 100644
index 00000000000..c0192bd6709
--- /dev/null
+++ b/doc/development/instrumentation.md
@@ -0,0 +1,37 @@
+# Instrumenting Ruby Code
+
+GitLab Performance Monitoring allows instrumenting of custom blocks of Ruby
+code. This can be used to measure the time spent in a specific part of a larger
+chunk of code. The resulting data is written to a separate series.
+
+To start measuring a block of Ruby code you should use
+`Gitlab::Metrics.measure` and give it a name for the series to store the data
+in:
+
+```ruby
+Gitlab::Metrics.measure(:user_logins) do
+ ...
+end
+```
+
+The first argument of this method is the series name and should be plural. This
+name will be prefixed with `rails_` or `sidekiq_` depending on whether the code
+was run in the Rails application or one of the Sidekiq workers. In the
+above example the final series names would be as follows:
+
+- rails_user_logins
+- sidekiq_user_logins
+
+Series names should be plural as this keeps the naming style in line with the
+other series names.
+
+By default metrics measured using a block contain a single value, "duration",
+which contains the number of milliseconds it took to execute the block. Custom
+values can be added by passing a Hash as the 2nd argument. Custom tags can be
+added by passing a Hash as the 3rd argument. A simple example is as follows:
+
+```ruby
+Gitlab::Metrics.measure(:example_series, { number: 10 }, { class: self.class.to_s }) do
+ ...
+end
+```
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 340fc5452ab..4c49442bf8b 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -15,7 +15,7 @@ module API
class User < UserBasic
expose :created_at
expose :is_admin?, as: :is_admin
- expose :bio, :skype, :linkedin, :twitter, :website_url
+ expose :bio, :location, :skype, :linkedin, :twitter, :website_url
end
class Identity < Grape::Entity
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 13ab17c6904..0a14bac07c0 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -58,6 +58,7 @@ module API
# extern_uid - External authentication provider UID
# provider - External provider
# bio - Bio
+ # location - Location of the user
# admin - User is admin - true or false (default)
# can_create_group - User can create groups - true or false
# confirm - Require user confirmation - true (default) or false
@@ -67,7 +68,7 @@ module API
post do
authenticated_as_admin!
required_attributes! [:email, :password, :name, :username]
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :can_create_group, :admin, :confirm, :external]
+ attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external]
admin = attrs.delete(:admin)
confirm = !(attrs.delete(:confirm) =~ (/(false|f|no|0)$/i))
user = User.build_user(attrs)
@@ -106,6 +107,7 @@ module API
# website_url - Website url
# projects_limit - Limit projects each user can create
# bio - Bio
+ # location - Location of the user
# admin - User is admin - true or false (default)
# can_create_group - User can create groups - true or false
# external - Flags the user as external - true or false(default)
@@ -114,7 +116,7 @@ module API
put ":id" do
authenticated_as_admin!
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :can_create_group, :admin, :external]
+ attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external]
user = User.find(params[:id])
not_found!('User') unless user
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index f21dbef216c..b8962379cb5 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -119,7 +119,7 @@ module Banzai
elsif element_node?(node)
yield_valid_link(node) do |link, text|
- if ref_pattern && link =~ /\A#{ref_pattern}/
+ if ref_pattern && link =~ /\A#{ref_pattern}\z/
replace_link_node_with_href(node, link) do
object_link_filter(link, ref_pattern, link_text: text)
end
diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb
index 88a265c6af2..4a3f47b5a95 100644
--- a/lib/gitlab/metrics.rb
+++ b/lib/gitlab/metrics.rb
@@ -70,6 +70,32 @@ module Gitlab
value.to_s.gsub('=', '\\=')
end
+ # Measures the execution time of a block.
+ #
+ # Example:
+ #
+ # Gitlab::Metrics.measure(:find_by_username_timings) do
+ # User.find_by_username(some_username)
+ # end
+ #
+ # series - The name of the series to store the data in.
+ # values - A Hash containing extra values to add to the metric.
+ # tags - A Hash containing extra tags to add to the metric.
+ #
+ # Returns the value yielded by the supplied block.
+ def self.measure(series, values = {}, tags = {})
+ return yield unless Transaction.current
+
+ start = Time.now.to_f
+ retval = yield
+ duration = (Time.now.to_f - start) * 1000.0
+ values = values.merge(duration: duration)
+
+ Transaction.current.add_metric(series, values, tags)
+
+ retval
+ end
+
# When enabled this should be set before being used as the usual pattern
# "@foo ||= bar" is _not_ thread-safe.
if enabled?
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
new file mode 100644
index 00000000000..90822a8c123
--- /dev/null
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -0,0 +1,119 @@
+require 'rails_helper'
+
+describe 'Filter issues', feature: true do
+
+ let!(:project) { create(:project) }
+ let!(:user) { create(:user)}
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:label) { create(:label, project: project) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ describe 'Filter issues for assignee from issues#index' do
+
+ before do
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('.js-assignee-search').click
+
+ find('.dropdown-menu-user-link', text: user.username).click
+
+ sleep 2
+ end
+
+ context 'assignee', js: true do
+ it 'should update to current user' do
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ end
+
+ it 'should not change when closed link is clicked' do
+ find('.issues-state-filters a', text: "Closed").click
+
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ end
+
+
+ it 'should not change when all link is clicked' do
+ find('.issues-state-filters a', text: "All").click
+
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ end
+ end
+ end
+
+ describe 'Filter issues for milestone from issues#index' do
+
+ before do
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('.js-milestone-select').click
+
+ find('.milestone-filter .dropdown-content a', text: milestone.title).click
+
+ sleep 2
+ end
+
+ context 'milestone', js: true do
+ it 'should update to current milestone' do
+ expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
+ end
+
+ it 'should not change when closed link is clicked' do
+ find('.issues-state-filters a', text: "Closed").click
+
+ expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
+ end
+
+
+ it 'should not change when all link is clicked' do
+ find('.issues-state-filters a', text: "All").click
+
+ expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
+ end
+ end
+ end
+
+ describe 'Filter issues for assignee and label from issues#index' do
+
+ before do
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('.js-assignee-search').click
+
+ find('.dropdown-menu-user-link', text: user.username).click
+
+ sleep 2
+
+ find('.js-label-select').click
+
+ find('.dropdown-menu-labels .dropdown-content a', text: label.title).click
+
+ sleep 2
+ end
+
+ context 'assignee and label', js: true do
+ it 'should update to current assignee and label' do
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
+ end
+
+ it 'should not change when closed link is clicked' do
+ find('.issues-state-filters a', text: "Closed").click
+
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
+ end
+
+
+ it 'should not change when all link is clicked' do
+ find('.issues-state-filters a', text: "All").click
+
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index 5a0d3d577a8..266ebef33d6 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -95,6 +95,14 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
result = reference_pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue]
end
+
+ it 'does not process links containing issue numbers followed by text' do
+ href = "#{reference}st"
+ doc = reference_filter("<a href='#{href}'></a>")
+ link = doc.css('a').first.attr('href')
+
+ expect(link).to eq(href)
+ end
end
context 'cross-project reference' do
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index 0ec8a6dc5cb..8f63a5f2043 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::Metrics do
end
end
- describe '#submit_metrics' do
+ describe '.submit_metrics' do
it 'prepares and writes the metrics to InfluxDB' do
connection = double(:connection)
pool = double(:pool)
@@ -26,7 +26,7 @@ describe Gitlab::Metrics do
end
end
- describe '#prepare_metrics' do
+ describe '.prepare_metrics' do
it 'returns a Hash with the keys as Symbols' do
metrics = described_class.
prepare_metrics([{ 'values' => {}, 'tags' => {} }])
@@ -51,7 +51,7 @@ describe Gitlab::Metrics do
end
end
- describe '#escape_value' do
+ describe '.escape_value' do
it 'escapes an equals sign' do
expect(described_class.escape_value('foo=')).to eq('foo\\=')
end
@@ -60,4 +60,45 @@ describe Gitlab::Metrics do
expect(described_class.escape_value(10)).to eq('10')
end
end
+
+ describe '.measure' do
+ context 'without a transaction' do
+ it 'returns the return value of the block' do
+ val = Gitlab::Metrics.measure(:foo) { 10 }
+
+ expect(val).to eq(10)
+ end
+ end
+
+ context 'with a transaction' do
+ let(:transaction) { Gitlab::Metrics::Transaction.new }
+
+ before do
+ allow(Gitlab::Metrics::Transaction).to receive(:current).
+ and_return(transaction)
+ end
+
+ it 'adds a metric to the current transaction' do
+ expect(transaction).to receive(:add_metric).
+ with(:foo, { duration: a_kind_of(Numeric) }, { tag: 'value' })
+
+ Gitlab::Metrics.measure(:foo, {}, tag: 'value') { 10 }
+ end
+
+ it 'supports adding of custom values' do
+ values = { duration: a_kind_of(Numeric), number: 10 }
+
+ expect(transaction).to receive(:add_metric).
+ with(:foo, values, { tag: 'value' })
+
+ Gitlab::Metrics.measure(:foo, { number: 10 }, tag: 'value') { 10 }
+ end
+
+ it 'returns the return value of the block' do
+ val = Gitlab::Metrics.measure(:foo) { 10 }
+
+ expect(val).to eq(10)
+ end
+ end
+ end
end
diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb
index 905379a64e3..2ccbff553f0 100644
--- a/spec/models/project_services/builds_email_service_spec.rb
+++ b/spec/models/project_services/builds_email_service_spec.rb
@@ -6,18 +6,38 @@ describe BuildsEmailService do
let(:service) { BuildsEmailService.new }
describe :execute do
- it "sends email" do
+ it 'sends email' do
service.recipients = 'test@gitlab.com'
data[:build_status] = 'failed'
expect(BuildEmailWorker).to receive(:perform_async)
service.execute(data)
end
- it "does not sends email with failed build and allowed_failure on" do
+ it 'does not send email with succeeded build and notify_only_broken_builds on' do
+ expect(service).to receive(:notify_only_broken_builds).and_return(true)
+ data[:build_status] = 'success'
+ expect(BuildEmailWorker).not_to receive(:perform_async)
+ service.execute(data)
+ end
+
+ it 'does not send email with failed build and build_allow_failure is true' do
data[:build_status] = 'failed'
data[:build_allow_failure] = true
expect(BuildEmailWorker).not_to receive(:perform_async)
service.execute(data)
end
+
+ it 'does not send email with unknown build status' do
+ data[:build_status] = 'foo'
+ expect(BuildEmailWorker).not_to receive(:perform_async)
+ service.execute(data)
+ end
+
+ it 'does not send email when recipients list is empty' do
+ service.recipients = ' ,, '
+ data[:build_status] = 'failed'
+ expect(BuildEmailWorker).not_to receive(:perform_async)
+ service.execute(data)
+ end
end
end