"
@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 c2defd3188..8d3ad934a5 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 4e6aa8cd1a..fcca9d4faf 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 3c74d25beb..f0f3744c6f 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 5e5e38a0ba..dbb6daf0d7 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 f6313255cb..f9f0483876 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 baa93ba4c1..6b208c3d0b 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 34fe1743f4..a681d6dece 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 383e7c76ab..7d2b4897cf 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 1b281809af..8940b558fb 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 0000000000..c0192bd670
--- /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 340fc5452a..4c49442bf8 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 13ab17c690..0a14bac07c 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 f21dbef216..b8962379cb 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 88a265c6af..4a3f47b5a9 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 0000000000..90822a8c12
--- /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 5a0d3d577a..266ebef33d 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("")
+ 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 0ec8a6dc5c..8f63a5f204 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 905379a64e..2ccbff553f 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