summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop.yml9
-rw-r--r--.rubocop_todo.yml7
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/build.js3
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js5
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js72
-rw-r--r--app/assets/stylesheets/framework/filters.scss3
-rw-r--r--app/controllers/admin/keys_controller.rb4
-rw-r--r--app/finders/todos_finder.rb2
-rw-r--r--app/helpers/dropdowns_helper.rb16
-rw-r--r--app/models/application_setting.rb4
-rw-r--r--app/models/commit.rb15
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/label.rb2
-rw-r--r--app/models/list.rb2
-rw-r--r--app/services/issuable_base_service.rb2
-rw-r--r--app/services/merge_requests/conflicts/resolve_service.rb12
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml3
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml4
-rw-r--r--changelogs/unreleased/32983-merge-conflict-resolution-removed-the-newline-in-the-end-of-file.yml4
-rw-r--r--changelogs/unreleased/jouve-gitlab-ce-admin_keys.yml5
-rw-r--r--changelogs/unreleased/winh-styled-people-search-bar.yml4
-rw-r--r--doc/user/profile/account/delete_account.md2
-rw-r--r--lib/api/helpers.rb2
-rw-r--r--lib/api/projects.rb4
-rw-r--r--lib/api/runner.rb2
-rw-r--r--lib/api/time_tracking_endpoints.rb2
-rw-r--r--lib/api/v3/projects.rb2
-rw-r--r--lib/api/v3/time_tracking_endpoints.rb2
-rw-r--r--lib/bitbucket/representation/pull_request_comment.rb4
-rw-r--r--lib/ci/api/builds.rb2
-rw-r--r--lib/gitlab/chat_commands/presenters/base.rb4
-rw-r--r--lib/gitlab/current_settings.rb5
-rw-r--r--lib/gitlab/git/diff.rb20
-rw-r--r--lib/gitlab/gitaly_client/commit.rb2
-rw-r--r--lib/gitlab/gitaly_client/diff.rb21
-rw-r--r--lib/gitlab/gitaly_client/diff_stitcher.rb31
-rw-r--r--lib/gitlab/google_code_import/client.rb2
-rw-r--r--lib/gitlab/google_code_import/importer.rb18
-rw-r--r--lib/gitlab/route_map.rb4
-rw-r--r--lib/gitlab/visibility_level.rb2
-rw-r--r--lib/gitlab/workhorse.rb2
-rw-r--r--spec/controllers/profiles/keys_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb2
-rw-r--r--spec/factories/ci/pipelines.rb21
-rw-r--r--spec/factories/ci/stages.rb2
-rw-r--r--spec/factories/ci/trigger_requests.rb4
-rw-r--r--spec/factories/commits.rb9
-rw-r--r--spec/factories/file_uploaders.rb (renamed from spec/factories/file_uploader.rb)2
-rw-r--r--spec/factories/keys.rb19
-rw-r--r--spec/factories/project_statistics.rb8
-rw-r--r--spec/factories/project_wikis.rb2
-rw-r--r--spec/factories/projects.rb2
-rw-r--r--spec/factories/wiki_directories.rb2
-rw-r--r--spec/factories_spec.rb14
-rw-r--r--spec/features/boards/modal_filter_spec.rb4
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb4
-rw-r--r--spec/javascripts/filtered_search/dropdown_utils_spec.js29
-rw-r--r--spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js406
-rw-r--r--spec/javascripts/fixtures/issues.rb11
-rw-r--r--spec/javascripts/helpers/filtered_search_spec_helper.js13
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb10
-rw-r--r--spec/lib/gitlab/gitaly_client/diff_spec.rb30
-rw-r--r--spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb59
-rw-r--r--spec/models/key_spec.rb10
-rw-r--r--spec/requests/api/v3/projects_spec.rb2
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb2
-rw-r--r--spec/services/merge_requests/conflicts/resolve_service_spec.rb42
-rw-r--r--spec/services/wiki_pages/create_service_spec.rb40
-rw-r--r--spec/services/wiki_pages/destroy_service_spec.rb19
-rw-r--r--spec/services/wiki_pages/update_service_spec.rb42
-rw-r--r--spec/support/helpers/key_generator_helper.rb41
74 files changed, 844 insertions, 332 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 3cdafd96456..8f611a96702 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -390,6 +390,15 @@ Style/OpMethod:
Style/ParenthesesAroundCondition:
Enabled: true
+# This cop (by default) checks for uses of methods Hash#has_key? and
+# Hash#has_value? where it enforces Hash#key? and Hash#value?
+# It is configurable to enforce the inverse, using `verbose` method
+# names also.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: short, verbose
+Style/PreferredHashMethods:
+ Enabled: true
+
# Checks for an obsolete RuntimeException argument in raise/fail.
Style/RedundantException:
Enabled: true
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index cf30f5728c0..e2d9c37479d 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -236,13 +236,6 @@ Style/PerlBackrefs:
Style/PredicateName:
Enabled: false
-# Offense count: 45
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: short, verbose
-Style/PreferredHashMethods:
- Enabled: false
-
# Offense count: 65
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
diff --git a/Gemfile b/Gemfile
index c49b60ffc23..56f5a8f6a41 100644
--- a/Gemfile
+++ b/Gemfile
@@ -368,7 +368,7 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client
-gem 'gitaly', '~> 0.7.0'
+gem 'gitaly', '~> 0.8.0'
gem 'toml-rb', '~> 0.3.15', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index f8adfec6143..be1f6555851 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -272,7 +272,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly (0.7.0)
+ gitaly (0.8.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -933,7 +933,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
- gitaly (~> 0.7.0)
+ gitaly (~> 0.8.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 1a602cbd8a7..072a899e9f2 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -64,7 +64,7 @@ window.Build = (function () {
$(window)
.off('resize.build')
- .on('resize.build', this.sidebarOnResize.bind(this));
+ .on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
this.updateArtifactRemoveDate();
@@ -250,6 +250,7 @@ window.Build = (function () {
Build.prototype.sidebarOnResize = function () {
this.toggleSidebar(this.shouldHideSidebarForViewport());
+
this.verifyTopPosition();
if (this.$scrollContainer.getNiceScroll(0)) {
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index 5c02a7a53d3..ef8fe071012 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -102,10 +102,13 @@ class DropdownUtils {
if (token.classList.contains('js-visual-token')) {
const name = token.querySelector('.name');
const value = token.querySelector('.value');
+ const valueContainer = token.querySelector('.value-container');
const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
let valueText = '';
- if (value && value.innerText) {
+ if (valueContainer && valueContainer.dataset.originalValue) {
+ valueText = valueContainer.dataset.originalValue;
+ } else if (value && value.innerText) {
valueText = value.innerText;
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index bc1226f5879..e9278140af0 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -1,6 +1,7 @@
-import AjaxCache from '~/lib/utils/ajax_cache';
-import '~/flash'; /* global Flash */
+import AjaxCache from '../lib/utils/ajax_cache';
+import '../flash'; /* global Flash */
import FilteredSearchContainer from './container';
+import UsersCache from '../lib/utils/users_cache';
class FilteredSearchVisualTokens {
static getLastVisualTokenBeforeInput() {
@@ -82,12 +83,42 @@ class FilteredSearchVisualTokens {
.catch(() => new Flash('An error occurred while fetching label colors.'));
}
+ static updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) {
+ if (tokenValue === 'none') {
+ return Promise.resolve();
+ }
+
+ const username = tokenValue.replace(/^@/, '');
+ return UsersCache.retrieve(username)
+ .then((user) => {
+ if (!user) {
+ return;
+ }
+
+ /* eslint-disable no-param-reassign */
+ tokenValueContainer.dataset.originalValue = tokenValue;
+ tokenValueElement.innerHTML = `
+ <img class="avatar s20" src="${user.avatar_url}" alt="${user.name}'s avatar">
+ ${user.name}
+ `;
+ /* eslint-enable no-param-reassign */
+ })
+ // ignore error and leave username in the search bar
+ .catch(() => { });
+ }
+
static renderVisualTokenValue(parentElement, tokenName, tokenValue) {
const tokenValueContainer = parentElement.querySelector('.value-container');
- tokenValueContainer.querySelector('.value').innerText = tokenValue;
+ const tokenValueElement = tokenValueContainer.querySelector('.value');
+ tokenValueElement.innerText = tokenValue;
- if (tokenName.toLowerCase() === 'label') {
+ const tokenType = tokenName.toLowerCase();
+ if (tokenType === 'label') {
FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue);
+ } else if ((tokenType === 'author') || (tokenType === 'assignee')) {
+ FilteredSearchVisualTokens.updateUserTokenAppearance(
+ tokenValueContainer, tokenValueElement, tokenValue,
+ );
}
}
@@ -153,6 +184,12 @@ class FilteredSearchVisualTokens {
if (!lastVisualToken) return '';
+ const valueContainer = lastVisualToken.querySelector('.value-container');
+ const originalValue = valueContainer && valueContainer.dataset.originalValue;
+ if (originalValue) {
+ return originalValue;
+ }
+
const value = lastVisualToken.querySelector('.value');
const name = lastVisualToken.querySelector('.name');
@@ -205,17 +242,28 @@ class FilteredSearchVisualTokens {
const inputLi = input.parentElement;
tokenContainer.replaceChild(inputLi, token);
- const name = token.querySelector('.name');
- const value = token.querySelector('.value');
+ const nameElement = token.querySelector('.name');
+ let value;
- if (token.classList.contains('filtered-search-token') && value) {
- FilteredSearchVisualTokens.addFilterVisualToken(name.innerText);
- input.value = value.innerText;
- } else {
- // token is a search term
- input.value = name.innerText;
+ if (token.classList.contains('filtered-search-token')) {
+ FilteredSearchVisualTokens.addFilterVisualToken(nameElement.innerText);
+
+ const valueContainerElement = token.querySelector('.value-container');
+ value = valueContainerElement.dataset.originalValue;
+
+ if (!value) {
+ const valueElement = valueContainerElement.querySelector('.value');
+ value = valueElement.innerText;
+ }
}
+ // token is a search term
+ if (!value) {
+ value = nameElement.innerText;
+ }
+
+ input.value = value;
+
// Opens dropdown
const inputEvent = new Event('input');
input.dispatchEvent(inputEvent);
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 90051ffe753..585f4871f5f 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -90,6 +90,7 @@
.filtered-search-term {
display: -webkit-flex;
display: flex;
+ flex-shrink: 0;
margin-top: 5px;
margin-bottom: 5px;
@@ -239,7 +240,7 @@
width: 35px;
background-color: $white-light;
border: none;
- position: absolute;
+ position: static;
right: 0;
height: 100%;
outline: none;
diff --git a/app/controllers/admin/keys_controller.rb b/app/controllers/admin/keys_controller.rb
index 054bb52b696..299419fb509 100644
--- a/app/controllers/admin/keys_controller.rb
+++ b/app/controllers/admin/keys_controller.rb
@@ -15,9 +15,9 @@ class Admin::KeysController < Admin::ApplicationController
respond_to do |format|
if key.destroy
- format.html { redirect_to [:admin, user], notice: 'User key was successfully removed.' }
+ format.html { redirect_to keys_admin_user_path(user), notice: 'User key was successfully removed.' }
else
- format.html { redirect_to [:admin, user], alert: 'Failed to remove user key.' }
+ format.html { redirect_to keys_admin_user_path(user), alert: 'Failed to remove user key.' }
end
end
end
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index dc13386184e..c358f23f541 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -39,7 +39,7 @@ class TodosFinder
private
def action_id?
- action_id.present? && Todo::ACTION_NAMES.has_key?(action_id.to_i)
+ action_id.present? && Todo::ACTION_NAMES.key?(action_id.to_i)
end
def action_id
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index 8ed99642c7a..ac8c518ac84 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -1,27 +1,27 @@
module DropdownsHelper
def dropdown_tag(toggle_text, options: {}, &block)
- content_tag :div, class: "dropdown #{options[:wrapper_class] if options.has_key?(:wrapper_class)}" do
+ content_tag :div, class: "dropdown #{options[:wrapper_class] if options.key?(:wrapper_class)}" do
data_attr = { toggle: "dropdown" }
- if options.has_key?(:data)
+ if options.key?(:data)
data_attr = options[:data].merge(data_attr)
end
dropdown_output = dropdown_toggle(toggle_text, data_attr, options)
- dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.has_key?(:dropdown_class)}") do
+ dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.key?(:dropdown_class)}") do
output = ""
- if options.has_key?(:title)
+ if options.key?(:title)
output << dropdown_title(options[:title])
end
- if options.has_key?(:filter)
+ if options.key?(:filter)
output << dropdown_filter(options[:placeholder])
end
- output << content_tag(:div, class: "dropdown-content #{options[:content_class] if options.has_key?(:content_class)}") do
- capture(&block) if block && !options.has_key?(:footer_content)
+ output << content_tag(:div, class: "dropdown-content #{options[:content_class] if options.key?(:content_class)}") do
+ capture(&block) if block && !options.key?(:footer_content)
end
if block && options[:footer_content]
@@ -41,7 +41,7 @@ module DropdownsHelper
def dropdown_toggle(toggle_text, data_attr, options = {})
default_label = data_attr[:default_label]
- content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
+ content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.key?(:toggle_class)}", id: (options[:id] if options.key?(:id)), type: "button", data: data_attr) do
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}")
output << icon('chevron-down')
output.html_safe
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 3d12f3c306b..9e04976e8fd 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -143,7 +143,7 @@ class ApplicationSetting < ActiveRecord::Base
validates_each :restricted_visibility_levels do |record, attr, value|
value&.each do |level|
- unless Gitlab::VisibilityLevel.options.has_value?(level)
+ unless Gitlab::VisibilityLevel.options.value?(level)
record.errors.add(attr, "'#{level}' is not a valid visibility level")
end
end
@@ -151,7 +151,7 @@ class ApplicationSetting < ActiveRecord::Base
validates_each :import_sources do |record, attr, value|
value&.each do |source|
- unless Gitlab::ImportSources.options.has_value?(source)
+ unless Gitlab::ImportSources.options.value?(source)
record.errors.add(attr, "'#{source}' is not a import source")
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index dbc0a22829e..bfa3a624e70 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -14,7 +14,7 @@ class Commit
participant :committer
participant :notes_with_associations
- attr_accessor :project
+ attr_accessor :project, :author
DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
@@ -177,7 +177,7 @@ class Commit
if RequestStore.active?
key = "commit_author:#{author_email.downcase}"
# nil is a valid value since no author may exist in the system
- if RequestStore.store.has_key?(key)
+ if RequestStore.store.key?(key)
@author = RequestStore.store[key]
else
@author = find_author_by_any_email
@@ -326,11 +326,12 @@ class Commit
end
def raw_diffs(*args)
- if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
- Gitlab::GitalyClient::Commit.new(project.repository).diff_from_parent(self, *args)
- else
- raw.diffs(*args)
- end
+ # Uncomment when https://gitlab.com/gitlab-org/gitaly/merge_requests/170 is merged
+ # if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
+ # Gitlab::GitalyClient::Commit.new(project.repository).diff_from_parent(self, *args)
+ # else
+ raw.diffs(*args)
+ # end
end
def raw_deltas
diff --git a/app/models/issue.rb b/app/models/issue.rb
index a88dbb3e065..693cc21bb40 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -251,9 +251,9 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
- json[:subscribed] = subscribed?(options[:user], project) if options.has_key?(:user) && options[:user]
+ json[:subscribed] = subscribed?(options[:user], project) if options.key?(:user) && options[:user]
- if options.has_key?(:labels)
+ if options.key?(:labels)
json[:labels] = labels.as_json(
project: project,
only: [:id, :title, :description, :color, :priority],
diff --git a/app/models/label.rb b/app/models/label.rb
index 074239702f8..955d6b4079b 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -172,7 +172,7 @@ class Label < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
- json[:priority] = priority(options[:project]) if options.has_key?(:project)
+ json[:priority] = priority(options[:project]) if options.key?(:project)
end
end
diff --git a/app/models/list.rb b/app/models/list.rb
index fbd19acd1f5..ba7353a1325 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -28,7 +28,7 @@ class List < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
- if options.has_key?(:label)
+ if options.key?(:label)
json[:label] = label.as_json(
project: board.project,
only: [:id, :title, :description, :color]
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index e94ab3e64db..e77a3e3eac1 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -148,7 +148,7 @@ class IssuableBaseService < BaseService
execute(params[:description], issuable)
# Avoid a description already set on an issuable to be overwritten by a nil
- params[:description] = description if params.has_key?(:description)
+ params[:description] = description if params.key?(:description)
params.merge!(command_params)
end
diff --git a/app/services/merge_requests/conflicts/resolve_service.rb b/app/services/merge_requests/conflicts/resolve_service.rb
index d74a82effd6..c2c335b8461 100644
--- a/app/services/merge_requests/conflicts/resolve_service.rb
+++ b/app/services/merge_requests/conflicts/resolve_service.rb
@@ -37,11 +37,13 @@ module MergeRequests
private
def write_resolved_file_to_index(merge_index, rugged, file, params)
- new_file = if params[:sections]
- file.resolve_lines(params[:sections]).map(&:text).join("\n")
- elsif params[:content]
- file.resolve_content(params[:content])
- end
+ if params[:sections]
+ new_file = file.resolve_lines(params[:sections]).map(&:text).join("\n")
+
+ new_file << "\n" if file.our_blob.data.ends_with?("\n")
+ elsif params[:content]
+ new_file = file.resolve_content(params[:content])
+ end
our_path = file.our_path
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index 3e83142377b..f700b5c9455 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -130,6 +130,3 @@
= build.id
- if build.retried?
%i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
-
-:javascript
- new Sidebar();
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index a9a4792faae..be9f9ee29c4 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -26,8 +26,6 @@
%li.input-token
%input.form-control.filtered-search{ id: "filtered-search-#{type.to_s}", placeholder: 'Search or filter results...', data: { 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } }
= icon('filter')
- %button.clear-search.hidden{ type: 'button' }
- = icon('times')
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
@@ -95,6 +93,8 @@
%span.dropdown-label-box{ style: 'background: {{color}}' }
%span.label-title.js-data-value
{{title}}
+ %button.clear-search.hidden{ type: 'button' }
+ = icon('times')
.filter-dropdown-container
- if type == :boards
- if can?(current_user, :admin_list, @project)
diff --git a/changelogs/unreleased/32983-merge-conflict-resolution-removed-the-newline-in-the-end-of-file.yml b/changelogs/unreleased/32983-merge-conflict-resolution-removed-the-newline-in-the-end-of-file.yml
new file mode 100644
index 00000000000..eca42176501
--- /dev/null
+++ b/changelogs/unreleased/32983-merge-conflict-resolution-removed-the-newline-in-the-end-of-file.yml
@@ -0,0 +1,4 @@
+---
+title: Keep trailing newline when resolving conflicts by picking sides
+merge_request:
+author:
diff --git a/changelogs/unreleased/jouve-gitlab-ce-admin_keys.yml b/changelogs/unreleased/jouve-gitlab-ce-admin_keys.yml
new file mode 100644
index 00000000000..df4de9f4e21
--- /dev/null
+++ b/changelogs/unreleased/jouve-gitlab-ce-admin_keys.yml
@@ -0,0 +1,5 @@
+---
+title: Redirect to user's keys index instead of user's index after a key is deleted
+ in the admin
+merge_request: 10227
+author: Cyril Jouve
diff --git a/changelogs/unreleased/winh-styled-people-search-bar.yml b/changelogs/unreleased/winh-styled-people-search-bar.yml
new file mode 100644
index 00000000000..a088af37d8d
--- /dev/null
+++ b/changelogs/unreleased/winh-styled-people-search-bar.yml
@@ -0,0 +1,4 @@
+---
+title: Style people in issuable search bar
+merge_request: 11402
+author:
diff --git a/doc/user/profile/account/delete_account.md b/doc/user/profile/account/delete_account.md
index a61fb88de03..6e274a152e5 100644
--- a/doc/user/profile/account/delete_account.md
+++ b/doc/user/profile/account/delete_account.md
@@ -17,7 +17,7 @@ Here's a list of things that will not be deleted:
- Merge requests that the user created
- Notes that the user created
- Abuse reports that the user reported
-- Award emoji that the user craeted
+- Award emoji that the user created
Instead of being deleted, these records will be moved to a system-wide
"Ghost User", whose sole purpose is to act as a container for such records.
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 81f6fc3201d..2c73a6fdc4e 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -158,7 +158,7 @@ module API
params_hash = custom_params || params
attrs = {}
keys.each do |key|
- if params_hash[key].present? || (params_hash.has_key?(key) && params_hash[key] == false)
+ if params_hash[key].present? || (params_hash.key?(key) && params_hash[key] == false)
attrs[key] = params_hash[key]
end
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 17008aa6d0f..deac3934d57 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -109,7 +109,7 @@ module API
end
post do
attrs = declared_params(include_missing: false)
- attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.has_key?(:jobs_enabled)
+ attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.key?(:jobs_enabled)
project = ::Projects::CreateService.new(current_user, attrs).execute
if project.saved?
@@ -248,7 +248,7 @@ module API
authorize! :rename_project, user_project if attrs[:name].present?
authorize! :change_visibility_level, user_project if attrs[:visibility].present?
- attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.has_key?(:jobs_enabled)
+ attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.key?(:jobs_enabled)
result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 3fd0536dadd..4552115b3e2 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -141,7 +141,7 @@ module API
patch '/:id/trace' do
job = authenticate_job!
- error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range')
+ error!('400 Missing header Content-Range', 400) unless request.headers.key?('Content-Range')
content_range = request.headers['Content-Range']
content_range = content_range.split('-')
diff --git a/lib/api/time_tracking_endpoints.rb b/lib/api/time_tracking_endpoints.rb
index 05b4b490e27..df4632346dd 100644
--- a/lib/api/time_tracking_endpoints.rb
+++ b/lib/api/time_tracking_endpoints.rb
@@ -5,7 +5,7 @@ module API
included do
helpers do
def issuable_name
- declared_params.has_key?(:issue_iid) ? 'issue' : 'merge_request'
+ declared_params.key?(:issue_iid) ? 'issue' : 'merge_request'
end
def issuable_key
diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb
index 896c00b88e7..20976b9dd08 100644
--- a/lib/api/v3/projects.rb
+++ b/lib/api/v3/projects.rb
@@ -44,7 +44,7 @@ module API
end
def set_only_allow_merge_if_pipeline_succeeds!
- if params.has_key?(:only_allow_merge_if_build_succeeds)
+ if params.key?(:only_allow_merge_if_build_succeeds)
params[:only_allow_merge_if_pipeline_succeeds] = params.delete(:only_allow_merge_if_build_succeeds)
end
end
diff --git a/lib/api/v3/time_tracking_endpoints.rb b/lib/api/v3/time_tracking_endpoints.rb
index 81ae4e8137d..d5b90e435ba 100644
--- a/lib/api/v3/time_tracking_endpoints.rb
+++ b/lib/api/v3/time_tracking_endpoints.rb
@@ -6,7 +6,7 @@ module API
included do
helpers do
def issuable_name
- declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request'
+ declared_params.key?(:issue_id) ? 'issue' : 'merge_request'
end
def issuable_key
diff --git a/lib/bitbucket/representation/pull_request_comment.rb b/lib/bitbucket/representation/pull_request_comment.rb
index 4f8efe03bae..c52acbc3ddc 100644
--- a/lib/bitbucket/representation/pull_request_comment.rb
+++ b/lib/bitbucket/representation/pull_request_comment.rb
@@ -22,11 +22,11 @@ module Bitbucket
end
def inline?
- raw.has_key?('inline')
+ raw.key?('inline')
end
def has_parent?
- raw.has_key?('parent')
+ raw.key?('parent')
end
private
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 2285ef241d7..e2e91ce99cd 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -88,7 +88,7 @@ module Ci
patch ":id/trace.txt" do
build = authenticate_build!
- error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range')
+ error!('400 Missing header Content-Range', 400) unless request.headers.key?('Content-Range')
content_range = request.headers['Content-Range']
content_range = content_range.split('-')
diff --git a/lib/gitlab/chat_commands/presenters/base.rb b/lib/gitlab/chat_commands/presenters/base.rb
index 2700a5a2ad5..05994bee79d 100644
--- a/lib/gitlab/chat_commands/presenters/base.rb
+++ b/lib/gitlab/chat_commands/presenters/base.rb
@@ -45,9 +45,9 @@ module Gitlab
end
def format_response(response)
- response[:text] = format(response[:text]) if response.has_key?(:text)
+ response[:text] = format(response[:text]) if response.key?(:text)
- if response.has_key?(:attachments)
+ if response.key?(:attachments)
response[:attachments].each do |attachment|
attachment[:pretext] = format(attachment[:pretext]) if attachment[:pretext]
attachment[:text] = format(attachment[:text]) if attachment[:text]
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 82576d197fe..9e14b35b0f8 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -19,7 +19,7 @@ module Gitlab
settings = ::ApplicationSetting.last
end
- settings ||= ::ApplicationSetting.create_from_defaults unless ActiveRecord::Migrator.needs_migration?
+ settings ||= ::ApplicationSetting.create_from_defaults
end
settings || in_memory_application_settings
@@ -46,7 +46,8 @@ module Gitlab
active_db_connection = ActiveRecord::Base.connection.active? rescue false
active_db_connection &&
- ActiveRecord::Base.connection.table_exists?('application_settings')
+ ActiveRecord::Base.connection.table_exists?('application_settings') &&
+ !ActiveRecord::Migrator.needs_migration?
rescue ActiveRecord::NoDatabaseError
false
end
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index 0594ac8e213..7e21994a084 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -189,7 +189,7 @@ module Gitlab
prune_diff_if_eligible
when Rugged::Patch, Rugged::Diff::Delta
init_from_rugged(raw_diff)
- when Gitaly::CommitDiffResponse
+ when Gitlab::GitalyClient::Diff
init_from_gitaly(raw_diff)
prune_diff_if_eligible
when Gitaly::CommitDelta
@@ -290,15 +290,15 @@ module Gitlab
end
end
- def init_from_gitaly(msg)
- @diff = msg.raw_chunks.join if msg.respond_to?(:raw_chunks)
- @new_path = encode!(msg.to_path.dup)
- @old_path = encode!(msg.from_path.dup)
- @a_mode = msg.old_mode.to_s(8)
- @b_mode = msg.new_mode.to_s(8)
- @new_file = msg.from_id == BLANK_SHA
- @renamed_file = msg.from_path != msg.to_path
- @deleted_file = msg.to_id == BLANK_SHA
+ def init_from_gitaly(diff)
+ @diff = diff.patch if diff.respond_to?(:patch)
+ @new_path = encode!(diff.to_path.dup)
+ @old_path = encode!(diff.from_path.dup)
+ @a_mode = diff.old_mode.to_s(8)
+ @b_mode = diff.new_mode.to_s(8)
+ @new_file = diff.from_id == BLANK_SHA
+ @renamed_file = diff.from_path != diff.to_path
+ @deleted_file = diff.to_id == BLANK_SHA
end
def prune_diff_if_eligible
diff --git a/lib/gitlab/gitaly_client/commit.rb b/lib/gitlab/gitaly_client/commit.rb
index 4491903d788..ba3da781dad 100644
--- a/lib/gitlab/gitaly_client/commit.rb
+++ b/lib/gitlab/gitaly_client/commit.rb
@@ -26,7 +26,7 @@ module Gitlab
request_params[:ignore_whitespace_change] = options.fetch(:ignore_whitespace_change, false)
response = diff_service_stub.commit_diff(Gitaly::CommitDiffRequest.new(request_params))
- Gitlab::Git::DiffCollection.new(response, options)
+ Gitlab::Git::DiffCollection.new(GitalyClient::DiffStitcher.new(response), options)
end
def commit_deltas(commit)
diff --git a/lib/gitlab/gitaly_client/diff.rb b/lib/gitlab/gitaly_client/diff.rb
new file mode 100644
index 00000000000..1e117b7e74a
--- /dev/null
+++ b/lib/gitlab/gitaly_client/diff.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module GitalyClient
+ class Diff
+ FIELDS = %i(from_path to_path old_mode new_mode from_id to_id patch).freeze
+
+ attr_accessor(*FIELDS)
+
+ def initialize(params)
+ params.each do |key, val|
+ public_send(:"#{key}=", val)
+ end
+ end
+
+ def ==(other)
+ FIELDS.all? do |field|
+ public_send(field) == other.public_send(field)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/diff_stitcher.rb b/lib/gitlab/gitaly_client/diff_stitcher.rb
new file mode 100644
index 00000000000..d84e8d752dc
--- /dev/null
+++ b/lib/gitlab/gitaly_client/diff_stitcher.rb
@@ -0,0 +1,31 @@
+module Gitlab
+ module GitalyClient
+ class DiffStitcher
+ include Enumerable
+
+ def initialize(rpc_response)
+ @rpc_response = rpc_response
+ end
+
+ def each
+ current_diff = nil
+
+ @rpc_response.each do |diff_msg|
+ if current_diff.nil?
+ diff_params = diff_msg.to_h.slice(*GitalyClient::Diff::FIELDS)
+ diff_params[:patch] = diff_msg.raw_patch_data
+
+ current_diff = GitalyClient::Diff.new(diff_params)
+ else
+ current_diff.patch += diff_msg.raw_patch_data
+ end
+
+ if diff_msg.end_of_patch
+ yield current_diff
+ current_diff = nil
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/google_code_import/client.rb b/lib/gitlab/google_code_import/client.rb
index 890bd9a3554..b1dbf554e41 100644
--- a/lib/gitlab/google_code_import/client.rb
+++ b/lib/gitlab/google_code_import/client.rb
@@ -14,7 +14,7 @@ module Gitlab
end
def valid?
- raw_data.is_a?(Hash) && raw_data["kind"] == "projecthosting#user" && raw_data.has_key?("projects")
+ raw_data.is_a?(Hash) && raw_data["kind"] == "projecthosting#user" && raw_data.key?("projects")
end
def repos
diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb
index 1b43440673c..ab38c0c3e34 100644
--- a/lib/gitlab/google_code_import/importer.rb
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -95,7 +95,7 @@ module Gitlab
labels = import_issue_labels(raw_issue)
assignee_id = nil
- if raw_issue.has_key?("owner")
+ if raw_issue.key?("owner")
username = user_map[raw_issue["owner"]["name"]]
if username.start_with?("@")
@@ -144,7 +144,7 @@ module Gitlab
def import_issue_comments(issue, comments)
Note.transaction do
while raw_comment = comments.shift
- next if raw_comment.has_key?("deletedBy")
+ next if raw_comment.key?("deletedBy")
content = format_content(raw_comment["content"])
updates = format_updates(raw_comment["updates"])
@@ -235,15 +235,15 @@ module Gitlab
def format_updates(raw_updates)
updates = []
- if raw_updates.has_key?("status")
+ if raw_updates.key?("status")
updates << "*Status: #{raw_updates["status"]}*"
end
- if raw_updates.has_key?("owner")
+ if raw_updates.key?("owner")
updates << "*Owner: #{user_map[raw_updates["owner"]]}*"
end
- if raw_updates.has_key?("cc")
+ if raw_updates.key?("cc")
cc = raw_updates["cc"].map do |l|
deleted = l.start_with?("-")
l = l[1..-1] if deleted
@@ -255,7 +255,7 @@ module Gitlab
updates << "*Cc: #{cc.join(", ")}*"
end
- if raw_updates.has_key?("labels")
+ if raw_updates.key?("labels")
labels = raw_updates["labels"].map do |l|
deleted = l.start_with?("-")
l = l[1..-1] if deleted
@@ -267,11 +267,11 @@ module Gitlab
updates << "*Labels: #{labels.join(", ")}*"
end
- if raw_updates.has_key?("mergedInto")
+ if raw_updates.key?("mergedInto")
updates << "*Merged into: ##{raw_updates["mergedInto"]}*"
end
- if raw_updates.has_key?("blockedOn")
+ if raw_updates.key?("blockedOn")
blocked_ons = raw_updates["blockedOn"].map do |raw_blocked_on|
format_blocking_updates(raw_blocked_on)
end
@@ -279,7 +279,7 @@ module Gitlab
updates << "*Blocked on: #{blocked_ons.join(", ")}*"
end
- if raw_updates.has_key?("blocking")
+ if raw_updates.key?("blocking")
blockings = raw_updates["blocking"].map do |raw_blocked_on|
format_blocking_updates(raw_blocked_on)
end
diff --git a/lib/gitlab/route_map.rb b/lib/gitlab/route_map.rb
index 36791fae60f..877aa6e6a28 100644
--- a/lib/gitlab/route_map.rb
+++ b/lib/gitlab/route_map.rb
@@ -25,8 +25,8 @@ module Gitlab
def parse_entry(entry)
raise FormatError, 'Route map entry is not a hash' unless entry.is_a?(Hash)
- raise FormatError, 'Route map entry does not have a source key' unless entry.has_key?('source')
- raise FormatError, 'Route map entry does not have a public key' unless entry.has_key?('public')
+ raise FormatError, 'Route map entry does not have a source key' unless entry.key?('source')
+ raise FormatError, 'Route map entry does not have a public key' unless entry.key?('public')
source_pattern = entry['source']
public_path = entry['public']
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index 2e31f4462f9..85da4c8660b 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -83,7 +83,7 @@ module Gitlab
end
def valid_level?(level)
- options.has_value?(level)
+ options.value?(level)
end
def level_name(level)
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 18d8b4f4744..7f27317775c 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -129,7 +129,7 @@ module Gitlab
'MaxSessionTime' => terminal[:max_session_time]
}
}
- details['Terminal']['CAPem'] = terminal[:ca_pem] if terminal.has_key?(:ca_pem)
+ details['Terminal']['CAPem'] = terminal[:ca_pem] if terminal.key?(:ca_pem)
details
end
diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb
index 61e4fae46fb..363ed410bc0 100644
--- a/spec/controllers/profiles/keys_controller_spec.rb
+++ b/spec/controllers/profiles/keys_controller_spec.rb
@@ -49,7 +49,7 @@ describe Profiles::KeysController do
expect(response.body).to eq(user.all_ssh_keys.join("\n"))
expect(response.body).to include(key.key.sub(' dummy@gitlab.com', ''))
- expect(response.body).to include(another_key.key)
+ expect(response.body).to include(another_key.key.sub(' dummy@gitlab.com', ''))
expect(response.body).not_to include(deploy_key.key)
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 08024a2148b..a25db7a65fb 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -126,7 +126,7 @@ describe Projects::MergeRequestsController do
recorded = ActiveRecord::QueryRecorder.new { go(format: :json) }
- expect(recorded.count).to be_within(5).of(50)
+ expect(recorded.count).to be_within(5).of(59)
expect(recorded.cached_count).to eq(0)
end
end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index 03e3c62effe..35803f0c37f 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -9,14 +9,14 @@ FactoryGirl.define do
factory :ci_pipeline_without_jobs do
after(:build) do |pipeline|
- allow(pipeline).to receive(:ci_yaml_file) { YAML.dump({}) }
+ pipeline.instance_variable_set(:@ci_yaml_file, YAML.dump({}))
end
end
factory :ci_pipeline_with_one_job do
after(:build) do |pipeline|
allow(pipeline).to receive(:ci_yaml_file) do
- YAML.dump({ rspec: { script: "ls" } })
+ pipeline.instance_variable_set(:@ci_yaml_file, YAML.dump({ rspec: { script: "ls" } }))
end
end
end
@@ -34,17 +34,14 @@ FactoryGirl.define do
transient { config nil }
after(:build) do |pipeline, evaluator|
- allow(pipeline).to receive(:ci_yaml_file) do
- if evaluator.config
- YAML.dump(evaluator.config)
- else
- File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
- end
- end
+ if evaluator.config
+ pipeline.instance_variable_set(:@ci_yaml_file, YAML.dump(evaluator.config))
- # Populates pipeline with errors
- #
- pipeline.config_processor if evaluator.config
+ # Populates pipeline with errors
+ pipeline.config_processor if evaluator.config
+ else
+ pipeline.instance_variable_set(:@ci_yaml_file, File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')))
+ end
end
trait :invalid do
diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb
index 7f557b25ccb..d37eabb3e8c 100644
--- a/spec/factories/ci/stages.rb
+++ b/spec/factories/ci/stages.rb
@@ -1,5 +1,7 @@
FactoryGirl.define do
factory :ci_stage, class: Ci::Stage do
+ skip_create
+
transient do
name 'test'
status nil
diff --git a/spec/factories/ci/trigger_requests.rb b/spec/factories/ci/trigger_requests.rb
index b8d8fab0e0b..10e0ab4fd3c 100644
--- a/spec/factories/ci/trigger_requests.rb
+++ b/spec/factories/ci/trigger_requests.rb
@@ -1,8 +1,8 @@
FactoryGirl.define do
factory :ci_trigger_request, class: Ci::TriggerRequest do
- factory :ci_trigger_request_with_variables do
- trigger factory: :ci_trigger
+ trigger factory: :ci_trigger
+ factory :ci_trigger_request_with_variables do
variables do
{
TRIGGER_KEY_1: 'TRIGGER_VALUE_1',
diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb
index 89e260cf65b..36b9645438a 100644
--- a/spec/factories/commits.rb
+++ b/spec/factories/commits.rb
@@ -4,19 +4,14 @@ FactoryGirl.define do
factory :commit do
git_commit RepoHelpers.sample_commit
project factory: :empty_project
+ author { build(:author) }
initialize_with do
new(git_commit, project)
end
- after(:build) do |commit|
- allow(commit).to receive(:author).and_return build(:author)
- end
-
trait :without_author do
- after(:build) do |commit|
- allow(commit).to receive(:author).and_return nil
- end
+ author nil
end
end
end
diff --git a/spec/factories/file_uploader.rb b/spec/factories/file_uploaders.rb
index bc74aeecc3b..d397dd705a5 100644
--- a/spec/factories/file_uploader.rb
+++ b/spec/factories/file_uploaders.rb
@@ -1,5 +1,7 @@
FactoryGirl.define do
factory :file_uploader do
+ skip_create
+
project factory: :empty_project
secret nil
diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb
index 4e140102492..a13b6e3596e 100644
--- a/spec/factories/keys.rb
+++ b/spec/factories/keys.rb
@@ -1,27 +1,18 @@
+require_relative '../support/helpers/key_generator_helper'
+
FactoryGirl.define do
factory :key do
title
- key do
- 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0= dummy@gitlab.com'
- end
+ key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate + ' dummy@gitlab.com' }
- factory :deploy_key, class: 'DeployKey' do
- key do
- 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O96x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5/jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaCrzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy05qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz'
- end
- end
+ factory :deploy_key, class: 'DeployKey'
factory :personal_key do
user
end
factory :another_key do
- key do
- 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmTillFzNTrrGgwaCKaSj+QCz81E6jBc/s9av0+3b1Hwfxgkqjl4nAK/OD2NjgyrONDTDfR8cRN4eAAy6nY8GLkOyYBDyuc5nTMqs5z3yVuTwf3koGm/YQQCmo91psZ2BgDFTor8SVEE5Mm1D1k3JDMhDFxzzrOtRYFPci9lskTJaBjpqWZ4E9rDTD2q/QZntCqbC3wE9uSemRQB5f8kik7vD/AD8VQXuzKladrZKkzkONCPWsXDspUitjM8HkQdOf0PsYn1CMUC1xKYbCxkg5TkEosIwGv6CoEArUrdu/4+10LVslq494mAvEItywzrluCLCnwELfW+h/m8UHoVhZ'
- end
-
- factory :another_deploy_key, class: 'DeployKey' do
- end
+ factory :another_deploy_key, class: 'DeployKey'
end
factory :write_access_key, class: 'DeployKey' do
diff --git a/spec/factories/project_statistics.rb b/spec/factories/project_statistics.rb
index 72d43096216..6c2ed7c6581 100644
--- a/spec/factories/project_statistics.rb
+++ b/spec/factories/project_statistics.rb
@@ -1,6 +1,10 @@
FactoryGirl.define do
factory :project_statistics do
- project { create :project }
- namespace { project.namespace }
+ project
+
+ initialize_with do
+ # statistics are automatically created when a project is created
+ project&.statistics || new
+ end
end
end
diff --git a/spec/factories/project_wikis.rb b/spec/factories/project_wikis.rb
index a3403fd76ae..ae222d5e69a 100644
--- a/spec/factories/project_wikis.rb
+++ b/spec/factories/project_wikis.rb
@@ -1,5 +1,7 @@
FactoryGirl.define do
factory :project_wiki do
+ skip_create
+
project factory: :empty_project
user factory: :user
initialize_with { new(project, user) }
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index e8a9b688319..19a85e5a38f 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -1,3 +1,5 @@
+require_relative '../support/test_env'
+
FactoryGirl.define do
# Project without repository
#
diff --git a/spec/factories/wiki_directories.rb b/spec/factories/wiki_directories.rb
index 3f3c864ac2b..3b4cfc380b8 100644
--- a/spec/factories/wiki_directories.rb
+++ b/spec/factories/wiki_directories.rb
@@ -1,5 +1,7 @@
FactoryGirl.define do
factory :wiki_directory do
+ skip_create
+
slug '/path_up_to/dir'
initialize_with { new(slug) }
end
diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb
index 786e1456f5f..09b3c0b0994 100644
--- a/spec/factories_spec.rb
+++ b/spec/factories_spec.rb
@@ -3,14 +3,20 @@ require 'spec_helper'
describe 'factories' do
FactoryGirl.factories.each do |factory|
describe "#{factory.name} factory" do
- let(:entity) { build(factory.name) }
+ it 'does not raise error when built' do
+ expect { build(factory.name) }.not_to raise_error
+ end
it 'does not raise error when created' do
- expect { entity }.not_to raise_error
+ expect { create(factory.name) }.not_to raise_error
end
- it 'is valid', if: factory.build_class < ActiveRecord::Base do
- expect(entity).to be_valid
+ factory.definition.defined_traits.map(&:name).each do |trait_name|
+ describe "linting #{trait_name} trait" do
+ skip 'does not raise error when created' do
+ expect { create(factory.name, trait_name) }.not_to raise_error
+ end
+ end
end
end
end
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
index ce132bfd979..b6de6143354 100644
--- a/spec/features/boards/modal_filter_spec.rb
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -89,7 +89,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
page.within('.add-issues-modal') do
wait_for_requests
- expect(page).to have_selector('.js-visual-token', text: user2.username)
+ expect(page).to have_selector('.js-visual-token', text: user2.name)
expect(page).to have_selector('.card', count: 1)
end
end
@@ -125,7 +125,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
page.within('.add-issues-modal') do
wait_for_requests
- expect(page).to have_selector('.js-visual-token', text: user2.username)
+ expect(page).to have_selector('.js-visual-token', text: user2.name)
expect(page).to have_selector('.card', count: 1)
end
end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 7958ad7e24f..e5e4ba06b5a 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -6,7 +6,7 @@ describe 'Filter issues', js: true, feature: true do
let!(:group) { create(:group) }
let!(:project) { create(:project, group: group) }
- let!(:user) { create(:user, username: 'joe') }
+ let!(:user) { create(:user, username: 'joe', name: 'Joe') }
let!(:user2) { create(:user, username: 'jane') }
let!(:label) { create(:label, project: project) }
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index 96e87c82d2c..dbbafc9e004 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
describe 'Visual tokens', js: true, feature: true do
include FilteredSearchHelpers
+ include WaitForRequests
let!(:project) { create(:empty_project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') }
@@ -70,7 +71,8 @@ describe 'Visual tokens', js: true, feature: true do
end
it 'changes value in visual token' do
- expect(first('.tokens-container .filtered-search-token .value').text).to eq("@#{user_rock.username}")
+ wait_for_requests
+ expect(first('.tokens-container .filtered-search-token .value').text).to eq("#{user_rock.name}")
end
it 'moves input to the right' do
diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js
index bb02abdeea2..f55726379f3 100644
--- a/spec/javascripts/filtered_search/dropdown_utils_spec.js
+++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js
@@ -2,8 +2,12 @@ import '~/extensions/array';
import '~/filtered_search/dropdown_utils';
import '~/filtered_search/filtered_search_tokenizer';
import '~/filtered_search/filtered_search_dropdown_manager';
+import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
describe('Dropdown Utils', () => {
+ const issueListFixture = 'issues/issue_list.html.raw';
+ preloadFixtures(issueListFixture);
+
describe('getEscapedText', () => {
it('should return same word when it has no space', () => {
const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace');
@@ -314,4 +318,29 @@ describe('Dropdown Utils', () => {
});
});
});
+
+ describe('getSearchQuery', () => {
+ let authorToken;
+
+ beforeEach(() => {
+ loadFixtures(issueListFixture);
+
+ authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '@user');
+ const searchTermToken = FilteredSearchSpecHelper.createSearchVisualToken('search term');
+
+ const tokensContainer = document.querySelector('.tokens-container');
+ tokensContainer.appendChild(searchTermToken);
+ tokensContainer.appendChild(authorToken);
+ });
+
+ it('uses original value if present', () => {
+ const originalValue = 'original dance';
+ const valueContainer = authorToken.querySelector('.value-container');
+ valueContainer.dataset.originalValue = originalValue;
+
+ const searchQuery = gl.DropdownUtils.getSearchQuery();
+
+ expect(searchQuery).toBe(' search term author:original dance');
+ });
+ });
});
diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
index c5fa2b17106..fa4343ffbc8 100644
--- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
@@ -1,10 +1,22 @@
import AjaxCache from '~/lib/utils/ajax_cache';
+import UsersCache from '~/lib/utils/users_cache';
import '~/filtered_search/filtered_search_visual_tokens';
import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
describe('Filtered Search Visual Tokens', () => {
+ const subject = gl.FilteredSearchVisualTokens;
+
+ const findElements = (tokenElement) => {
+ const tokenNameElement = tokenElement.querySelector('.name');
+ const tokenValueContainer = tokenElement.querySelector('.value-container');
+ const tokenValueElement = tokenValueContainer.querySelector('.value');
+ return { tokenNameElement, tokenValueContainer, tokenValueElement };
+ };
+
let tokensContainer;
+ let authorToken;
+ let bugLabelToken;
beforeEach(() => {
setFixtures(`
@@ -13,12 +25,15 @@ describe('Filtered Search Visual Tokens', () => {
</ul>
`);
tokensContainer = document.querySelector('.tokens-container');
+
+ authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '@user');
+ bugLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~bug');
});
describe('getLastVisualTokenBeforeInput', () => {
it('returns when there are no visual tokens', () => {
const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ = subject.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(null);
expect(isLastVisualTokenValid).toEqual(true);
@@ -27,11 +42,11 @@ describe('Filtered Search Visual Tokens', () => {
describe('input is the last item in tokensContainer', () => {
it('returns when there is one visual token', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
- FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
+ bugLabelToken.outerHTML,
);
const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ = subject.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
expect(isLastVisualTokenValid).toEqual(true);
@@ -43,7 +58,7 @@ describe('Filtered Search Visual Tokens', () => {
);
const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ = subject.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
expect(isLastVisualTokenValid).toEqual(false);
@@ -51,13 +66,13 @@ describe('Filtered Search Visual Tokens', () => {
it('returns when there are multiple visual tokens', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${bugLabelToken.outerHTML}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
`);
const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ = subject.getLastVisualTokenBeforeInput();
const items = document.querySelectorAll('.tokens-container .js-visual-token');
expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true);
@@ -66,13 +81,13 @@ describe('Filtered Search Visual Tokens', () => {
it('returns when there are multiple visual tokens and an incomplete visual token', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${bugLabelToken.outerHTML}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee')}
`);
const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ = subject.getLastVisualTokenBeforeInput();
const items = document.querySelectorAll('.tokens-container .js-visual-token');
expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true);
@@ -83,13 +98,13 @@ describe('Filtered Search Visual Tokens', () => {
describe('input is a middle item in tokensContainer', () => {
it('returns last token before input', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${bugLabelToken.outerHTML}
${FilteredSearchSpecHelper.createInputHTML()}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
`);
const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ = subject.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
expect(isLastVisualTokenValid).toEqual(true);
@@ -103,7 +118,7 @@ describe('Filtered Search Visual Tokens', () => {
`);
const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ = subject.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
expect(isLastVisualTokenValid).toEqual(false);
@@ -114,7 +129,7 @@ describe('Filtered Search Visual Tokens', () => {
describe('unselectTokens', () => {
it('does nothing when there are no tokens', () => {
const beforeHTML = tokensContainer.innerHTML;
- gl.FilteredSearchVisualTokens.unselectTokens();
+ subject.unselectTokens();
expect(tokensContainer.innerHTML).toEqual(beforeHTML);
});
@@ -128,7 +143,7 @@ describe('Filtered Search Visual Tokens', () => {
const selected = tokensContainer.querySelector('.js-visual-token .selected');
expect(selected.classList.contains('selected')).toEqual(true);
- gl.FilteredSearchVisualTokens.unselectTokens();
+ subject.unselectTokens();
expect(selected.classList.contains('selected')).toEqual(false);
});
@@ -137,7 +152,7 @@ describe('Filtered Search Visual Tokens', () => {
describe('selectToken', () => {
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${bugLabelToken.outerHTML}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
`);
@@ -147,7 +162,7 @@ describe('Filtered Search Visual Tokens', () => {
const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable');
firstTokenButton.classList.add('selected');
- gl.FilteredSearchVisualTokens.selectToken(firstTokenButton);
+ subject.selectToken(firstTokenButton);
expect(firstTokenButton.classList.contains('selected')).toEqual(false);
});
@@ -156,7 +171,7 @@ describe('Filtered Search Visual Tokens', () => {
it('adds selected class', () => {
const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable');
- gl.FilteredSearchVisualTokens.selectToken(firstTokenButton);
+ subject.selectToken(firstTokenButton);
expect(firstTokenButton.classList.contains('selected')).toEqual(true);
});
@@ -165,7 +180,7 @@ describe('Filtered Search Visual Tokens', () => {
const tokenButtons = tokensContainer.querySelectorAll('.js-visual-token .selectable');
tokenButtons[1].classList.add('selected');
- gl.FilteredSearchVisualTokens.selectToken(tokenButtons[0]);
+ subject.selectToken(tokenButtons[0]);
expect(tokenButtons[0].classList.contains('selected')).toEqual(true);
expect(tokenButtons[1].classList.contains('selected')).toEqual(false);
@@ -181,7 +196,7 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
- gl.FilteredSearchVisualTokens.removeSelectedToken();
+ subject.removeSelectedToken();
expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
});
@@ -193,7 +208,7 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
- gl.FilteredSearchVisualTokens.removeSelectedToken();
+ subject.removeSelectedToken();
expect(tokensContainer.querySelector('.js-visual-token .selectable')).toEqual(null);
});
@@ -205,7 +220,7 @@ describe('Filtered Search Visual Tokens', () => {
beforeEach(() => {
setFixtures(`
<div class="test-area">
- ${gl.FilteredSearchVisualTokens.createVisualTokenElementHTML()}
+ ${subject.createVisualTokenElementHTML()}
</div>
`);
@@ -245,7 +260,7 @@ describe('Filtered Search Visual Tokens', () => {
describe('addVisualTokenElement', () => {
it('renders search visual tokens', () => {
- gl.FilteredSearchVisualTokens.addVisualTokenElement('search term', null, true);
+ subject.addVisualTokenElement('search term', null, true);
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-term')).toEqual(true);
@@ -254,7 +269,7 @@ describe('Filtered Search Visual Tokens', () => {
});
it('renders filter visual token name', () => {
- gl.FilteredSearchVisualTokens.addVisualTokenElement('milestone');
+ subject.addVisualTokenElement('milestone');
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
@@ -263,7 +278,7 @@ describe('Filtered Search Visual Tokens', () => {
});
it('renders filter visual token name and value', () => {
- gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend');
+ subject.addVisualTokenElement('label', 'Frontend');
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
@@ -274,7 +289,7 @@ describe('Filtered Search Visual Tokens', () => {
it('inserts visual token before input', () => {
tokensContainer.appendChild(FilteredSearchSpecHelper.createFilterVisualToken('assignee', '@root'));
- gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend');
+ subject.addVisualTokenElement('label', 'Frontend');
const tokens = tokensContainer.querySelectorAll('.js-visual-token');
const labelToken = tokens[0];
const assigneeToken = tokens[1];
@@ -296,7 +311,7 @@ describe('Filtered Search Visual Tokens', () => {
);
const original = tokensContainer.innerHTML;
- gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
+ subject.addValueToPreviousVisualTokenElement('value');
expect(original).toEqual(tokensContainer.innerHTML);
});
@@ -308,7 +323,7 @@ describe('Filtered Search Visual Tokens', () => {
`);
const original = tokensContainer.innerHTML;
- gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
+ subject.addValueToPreviousVisualTokenElement('value');
expect(original).toEqual(tokensContainer.innerHTML);
});
@@ -319,7 +334,7 @@ describe('Filtered Search Visual Tokens', () => {
);
const original = tokensContainer.innerHTML;
- gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
+ subject.addValueToPreviousVisualTokenElement('value');
const updatedToken = tokensContainer.querySelector('.js-visual-token');
expect(updatedToken.querySelector('.name').innerText).toEqual('label');
@@ -330,7 +345,7 @@ describe('Filtered Search Visual Tokens', () => {
describe('addFilterVisualToken', () => {
it('creates visual token with just tokenName', () => {
- gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone');
+ subject.addFilterVisualToken('milestone');
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
@@ -339,8 +354,8 @@ describe('Filtered Search Visual Tokens', () => {
});
it('creates visual token with just tokenValue', () => {
- gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone');
- gl.FilteredSearchVisualTokens.addFilterVisualToken('%8.17');
+ subject.addFilterVisualToken('milestone');
+ subject.addFilterVisualToken('%8.17');
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
@@ -349,7 +364,7 @@ describe('Filtered Search Visual Tokens', () => {
});
it('creates full visual token', () => {
- gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', '@john');
+ subject.addFilterVisualToken('assignee', '@john');
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
@@ -360,7 +375,7 @@ describe('Filtered Search Visual Tokens', () => {
describe('addSearchVisualToken', () => {
it('creates search visual token', () => {
- gl.FilteredSearchVisualTokens.addSearchVisualToken('search term');
+ subject.addSearchVisualToken('search term');
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-term')).toEqual(true);
@@ -374,7 +389,7 @@ describe('Filtered Search Visual Tokens', () => {
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
`);
- gl.FilteredSearchVisualTokens.addSearchVisualToken('append this');
+ subject.addSearchVisualToken('append this');
const token = tokensContainer.querySelector('.filtered-search-term');
expect(token.querySelector('.name').innerText).toEqual('search term append this');
@@ -386,10 +401,26 @@ describe('Filtered Search Visual Tokens', () => {
it('should get last token value', () => {
const value = '~bug';
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
- FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', value),
+ bugLabelToken.outerHTML,
+ );
+
+ expect(subject.getLastTokenPartial()).toEqual(value);
+ });
+
+ it('should get last token original value if available', () => {
+ const originalValue = '@user';
+ const valueContainer = authorToken.querySelector('.value-container');
+ valueContainer.dataset.originalValue = originalValue;
+ const avatar = document.createElement('img');
+ const valueElement = valueContainer.querySelector('.value');
+ valueElement.insertAdjacentElement('afterbegin', avatar);
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ authorToken.outerHTML,
);
- expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(value);
+ const lastTokenValue = subject.getLastTokenPartial();
+
+ expect(lastTokenValue).toEqual(originalValue);
});
it('should get last token name if there is no value', () => {
@@ -398,11 +429,11 @@ describe('Filtered Search Visual Tokens', () => {
FilteredSearchSpecHelper.createNameFilterVisualTokenHTML(name),
);
- expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(name);
+ expect(subject.getLastTokenPartial()).toEqual(name);
});
it('should return empty when there are no tokens', () => {
- expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual('');
+ expect(subject.getLastTokenPartial()).toEqual('');
});
});
@@ -414,7 +445,7 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokensContainer.querySelector('.js-visual-token .value')).not.toEqual(null);
- gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+ subject.removeLastTokenPartial();
expect(tokensContainer.querySelector('.js-visual-token .value')).toEqual(null);
});
@@ -426,14 +457,14 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokensContainer.querySelector('.js-visual-token .name')).not.toEqual(null);
- gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+ subject.removeLastTokenPartial();
expect(tokensContainer.querySelector('.js-visual-token .name')).toEqual(null);
});
it('should not remove anything when there are no tokens', () => {
const html = tokensContainer.innerHTML;
- gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+ subject.removeLastTokenPartial();
expect(tokensContainer.innerHTML).toEqual(html);
});
@@ -442,7 +473,7 @@ describe('Filtered Search Visual Tokens', () => {
describe('tokenizeInput', () => {
it('does not do anything if there is no input', () => {
const original = tokensContainer.innerHTML;
- gl.FilteredSearchVisualTokens.tokenizeInput();
+ subject.tokenizeInput();
expect(tokensContainer.innerHTML).toEqual(original);
});
@@ -454,7 +485,7 @@ describe('Filtered Search Visual Tokens', () => {
const input = document.querySelector('.filtered-search');
input.value = 'some value';
- gl.FilteredSearchVisualTokens.tokenizeInput();
+ subject.tokenizeInput();
const newToken = tokensContainer.querySelector('.filtered-search-term');
@@ -470,7 +501,7 @@ describe('Filtered Search Visual Tokens', () => {
const input = document.querySelector('.filtered-search');
input.value = '@john';
- gl.FilteredSearchVisualTokens.tokenizeInput();
+ subject.tokenizeInput();
const updatedToken = tokensContainer.querySelector('.filtered-search-token');
@@ -497,29 +528,39 @@ describe('Filtered Search Visual Tokens', () => {
it('tokenize\'s existing input', () => {
input.value = 'some text';
- spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callThrough();
+ spyOn(subject, 'tokenizeInput').and.callThrough();
- gl.FilteredSearchVisualTokens.editToken(token);
+ subject.editToken(token);
- expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled();
+ expect(subject.tokenizeInput).toHaveBeenCalled();
expect(input.value).not.toEqual('some text');
});
it('moves input to the token position', () => {
expect(tokensContainer.children[3].querySelector('.filtered-search')).not.toEqual(null);
- gl.FilteredSearchVisualTokens.editToken(token);
+ subject.editToken(token);
expect(tokensContainer.children[1].querySelector('.filtered-search')).not.toEqual(null);
expect(tokensContainer.children[3].querySelector('.filtered-search')).toEqual(null);
});
it('input contains the visual token value', () => {
- gl.FilteredSearchVisualTokens.editToken(token);
+ subject.editToken(token);
expect(input.value).toEqual('none');
});
+ it('input contains the original value if present', () => {
+ const originalValue = '@user';
+ const valueContainer = token.querySelector('.value-container');
+ valueContainer.dataset.originalValue = originalValue;
+
+ subject.editToken(token);
+
+ expect(input.value).toEqual(originalValue);
+ });
+
describe('selected token is a search term token', () => {
beforeEach(() => {
token = document.querySelector('.filtered-search-term');
@@ -528,7 +569,7 @@ describe('Filtered Search Visual Tokens', () => {
it('token is removed', () => {
expect(tokensContainer.querySelector('.filtered-search-term')).not.toEqual(null);
- gl.FilteredSearchVisualTokens.editToken(token);
+ subject.editToken(token);
expect(tokensContainer.querySelector('.filtered-search-term')).toEqual(null);
});
@@ -536,7 +577,7 @@ describe('Filtered Search Visual Tokens', () => {
it('input has the same value as removed token', () => {
expect(input.value).toEqual('');
- gl.FilteredSearchVisualTokens.editToken(token);
+ subject.editToken(token);
expect(input.value).toEqual('search');
});
@@ -549,25 +590,25 @@ describe('Filtered Search Visual Tokens', () => {
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none'),
);
- spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callFake(() => {});
- spyOn(gl.FilteredSearchVisualTokens, 'getLastVisualTokenBeforeInput').and.callThrough();
+ spyOn(subject, 'tokenizeInput').and.callFake(() => {});
+ spyOn(subject, 'getLastVisualTokenBeforeInput').and.callThrough();
- gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ subject.moveInputToTheRight();
- expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled();
- expect(gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput).not.toHaveBeenCalled();
+ expect(subject.tokenizeInput).toHaveBeenCalled();
+ expect(subject.getLastVisualTokenBeforeInput).not.toHaveBeenCalled();
});
it('tokenize\'s input', () => {
tokensContainer.innerHTML = `
${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')}
${FilteredSearchSpecHelper.createInputHTML()}
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${bugLabelToken.outerHTML}
`;
document.querySelector('.filtered-search').value = 'none';
- gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ subject.moveInputToTheRight();
const value = tokensContainer.querySelector('.js-visual-token .value');
expect(value.innerText).toEqual('none');
@@ -577,12 +618,12 @@ describe('Filtered Search Visual Tokens', () => {
tokensContainer.innerHTML = `
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
${FilteredSearchSpecHelper.createInputHTML()}
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${bugLabelToken.outerHTML}
`;
document.querySelector('.filtered-search').value = 'test';
- gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ subject.moveInputToTheRight();
const searchValue = tokensContainer.querySelector('.filtered-search-term .name');
expect(searchValue.innerText).toEqual('test');
@@ -592,10 +633,10 @@ describe('Filtered Search Visual Tokens', () => {
tokensContainer.innerHTML = `
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
${FilteredSearchSpecHelper.createInputHTML()}
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${bugLabelToken.outerHTML}
`;
- gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ subject.moveInputToTheRight();
expect(tokensContainer.children[2].querySelector('.filtered-search')).not.toEqual(null);
});
@@ -607,7 +648,7 @@ describe('Filtered Search Visual Tokens', () => {
${FilteredSearchSpecHelper.createInputHTML('', '~bug')}
`;
- gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ subject.moveInputToTheRight();
const token = tokensContainer.children[1];
expect(token.querySelector('.value').innerText).toEqual('~bug');
@@ -615,42 +656,144 @@ describe('Filtered Search Visual Tokens', () => {
});
describe('renderVisualTokenValue', () => {
- let searchTokens;
+ const keywordToken = FilteredSearchSpecHelper.createFilterVisualToken('search');
+ const milestoneToken = FilteredSearchSpecHelper.createFilterVisualToken('milestone', 'upcoming');
+
+ let updateLabelTokenColorSpy;
+ let updateUserTokenAppearanceSpy;
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
- ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search')}
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'upcoming')}
+ ${authorToken.outerHTML}
+ ${bugLabelToken.outerHTML}
+ ${keywordToken.outerHTML}
+ ${milestoneToken.outerHTML}
`);
- searchTokens = document.querySelectorAll('.filtered-search-token');
+ spyOn(subject, 'updateLabelTokenColor');
+ updateLabelTokenColorSpy = subject.updateLabelTokenColor;
+
+ spyOn(subject, 'updateUserTokenAppearance');
+ updateUserTokenAppearanceSpy = subject.updateUserTokenAppearance;
});
- it('renders a token value element', () => {
- spyOn(gl.FilteredSearchVisualTokens, 'updateLabelTokenColor');
- const updateLabelTokenColorSpy = gl.FilteredSearchVisualTokens.updateLabelTokenColor;
+ it('renders a author token value element', () => {
+ const { tokenNameElement, tokenValueContainer, tokenValueElement } =
+ findElements(authorToken);
+ const tokenName = tokenNameElement.innerText;
+ const tokenValue = 'new value';
- expect(searchTokens.length).toBe(2);
- Array.prototype.forEach.call(searchTokens, (token) => {
- updateLabelTokenColorSpy.calls.reset();
+ subject.renderVisualTokenValue(authorToken, tokenName, tokenValue);
- const tokenName = token.querySelector('.name').innerText;
- const tokenValue = 'new value';
- gl.FilteredSearchVisualTokens.renderVisualTokenValue(token, tokenName, tokenValue);
+ expect(tokenValueElement.innerText).toBe(tokenValue);
+ expect(updateUserTokenAppearanceSpy.calls.count()).toBe(1);
+ const expectedArgs = [tokenValueContainer, tokenValueElement, tokenValue];
+ expect(updateUserTokenAppearanceSpy.calls.argsFor(0)).toEqual(expectedArgs);
+ expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
+ });
- const tokenValueElement = token.querySelector('.value');
- expect(tokenValueElement.innerText).toBe(tokenValue);
+ it('renders a label token value element', () => {
+ const { tokenNameElement, tokenValueContainer, tokenValueElement } =
+ findElements(bugLabelToken);
+ const tokenName = tokenNameElement.innerText;
+ const tokenValue = 'new value';
- if (tokenName.toLowerCase() === 'label') {
- const tokenValueContainer = token.querySelector('.value-container');
- expect(updateLabelTokenColorSpy.calls.count()).toBe(1);
- const expectedArgs = [tokenValueContainer, tokenValue];
- expect(updateLabelTokenColorSpy.calls.argsFor(0)).toEqual(expectedArgs);
- } else {
- expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
- }
- });
+ subject.renderVisualTokenValue(bugLabelToken, tokenName, tokenValue);
+
+ expect(tokenValueElement.innerText).toBe(tokenValue);
+ expect(updateLabelTokenColorSpy.calls.count()).toBe(1);
+ const expectedArgs = [tokenValueContainer, tokenValue];
+ expect(updateLabelTokenColorSpy.calls.argsFor(0)).toEqual(expectedArgs);
+ expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
+ });
+
+ it('renders a milestone token value element', () => {
+ const { tokenNameElement, tokenValueElement } = findElements(milestoneToken);
+ const tokenName = tokenNameElement.innerText;
+ const tokenValue = 'new value';
+
+ subject.renderVisualTokenValue(milestoneToken, tokenName, tokenValue);
+
+ expect(tokenValueElement.innerText).toBe(tokenValue);
+ expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
+ expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
+ });
+ });
+
+ describe('updateUserTokenAppearance', () => {
+ let usersCacheSpy;
+
+ beforeEach(() => {
+ spyOn(UsersCache, 'retrieve').and.callFake(username => usersCacheSpy(username));
+ });
+
+ it('ignores special value "none"', (done) => {
+ usersCacheSpy = (username) => {
+ expect(username).toBe('none');
+ done.fail('Should not resolve "none"!');
+ };
+ const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
+
+ subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, 'none')
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('ignores error if UsersCache throws', (done) => {
+ spyOn(window, 'Flash');
+ const dummyError = new Error('Earth rotated backwards');
+ const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
+ const tokenValue = tokenValueElement.innerText;
+ usersCacheSpy = (username) => {
+ expect(`@${username}`).toBe(tokenValue);
+ return Promise.reject(dummyError);
+ };
+
+ subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
+ .then(() => {
+ expect(window.Flash.calls.count()).toBe(0);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does nothing if user cannot be found', (done) => {
+ const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
+ const tokenValue = tokenValueElement.innerText;
+ usersCacheSpy = (username) => {
+ expect(`@${username}`).toBe(tokenValue);
+ return Promise.resolve(undefined);
+ };
+
+ subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
+ .then(() => {
+ expect(tokenValueElement.innerText).toBe(tokenValue);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('replaces author token with avatar and display name', (done) => {
+ const dummyUser = {
+ name: 'Important Person',
+ avatar_url: 'https://host.invalid/mypics/avatar.png',
+ };
+ const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
+ const tokenValue = tokenValueElement.innerText;
+ usersCacheSpy = (username) => {
+ expect(`@${username}`).toBe(tokenValue);
+ return Promise.resolve(dummyUser);
+ };
+
+ subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
+ .then(() => {
+ expect(tokenValueContainer.dataset.originalValue).toBe(tokenValue);
+ expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
+ const avatar = tokenValueElement.querySelector('img.avatar');
+ expect(avatar.src).toBe(dummyUser.avatar_url);
+ })
+ .then(done)
+ .catch(done.fail);
});
});
@@ -659,21 +802,16 @@ describe('Filtered Search Visual Tokens', () => {
const dummyEndpoint = '/dummy/endpoint';
preloadFixtures(jsonFixtureName);
- const labelData = getJSONFixture(jsonFixtureName);
- const findLabel = tokenValue => labelData.find(
- label => tokenValue === `~${gl.DropdownUtils.getEscapedText(label.title)}`,
- );
- const bugLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~bug');
+ let labelData;
+
+ beforeAll(() => {
+ labelData = getJSONFixture(jsonFixtureName);
+ });
+
const missingLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~doesnotexist');
const spaceLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~"some space"');
- const parseColor = (color) => {
- const dummyElement = document.createElement('div');
- dummyElement.style.color = color;
- return dummyElement.style.color;
- };
-
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${bugLabelToken.outerHTML}
@@ -688,28 +826,60 @@ describe('Filtered Search Visual Tokens', () => {
AjaxCache.internalStorage[`${dummyEndpoint}/labels.json`] = labelData;
});
- const testCase = (token, done) => {
- const tokenValueContainer = token.querySelector('.value-container');
- const tokenValue = token.querySelector('.value').innerText;
- const label = findLabel(tokenValue);
+ const parseColor = (color) => {
+ const dummyElement = document.createElement('div');
+ dummyElement.style.color = color;
+ return dummyElement.style.color;
+ };
- gl.FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue)
- .then(() => {
- if (label) {
- expect(tokenValueContainer.getAttribute('style')).not.toBe(null);
- expect(tokenValueContainer.style.backgroundColor).toBe(parseColor(label.color));
- expect(tokenValueContainer.style.color).toBe(parseColor(label.text_color));
- } else {
- expect(token).toBe(missingLabelToken);
- expect(tokenValueContainer.getAttribute('style')).toBe(null);
- }
- })
- .then(done)
- .catch(fail);
+ const expectValueContainerStyle = (tokenValueContainer, label) => {
+ expect(tokenValueContainer.getAttribute('style')).not.toBe(null);
+ expect(tokenValueContainer.style.backgroundColor).toBe(parseColor(label.color));
+ expect(tokenValueContainer.style.color).toBe(parseColor(label.text_color));
};
- it('updates the color of a label token', done => testCase(bugLabelToken, done));
- it('updates the color of a label token with spaces', done => testCase(spaceLabelToken, done));
- it('does not change color of a missing label', done => testCase(missingLabelToken, done));
+ const findLabel = tokenValue => labelData.find(
+ label => tokenValue === `~${gl.DropdownUtils.getEscapedText(label.title)}`,
+ );
+
+ it('updates the color of a label token', (done) => {
+ const { tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
+ const tokenValue = tokenValueElement.innerText;
+ const matchingLabel = findLabel(tokenValue);
+
+ subject.updateLabelTokenColor(tokenValueContainer, tokenValue)
+ .then(() => {
+ expectValueContainerStyle(tokenValueContainer, matchingLabel);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('updates the color of a label token with spaces', (done) => {
+ const { tokenValueContainer, tokenValueElement } = findElements(spaceLabelToken);
+ const tokenValue = tokenValueElement.innerText;
+ const matchingLabel = findLabel(tokenValue);
+
+ subject.updateLabelTokenColor(tokenValueContainer, tokenValue)
+ .then(() => {
+ expectValueContainerStyle(tokenValueContainer, matchingLabel);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not change color of a missing label', (done) => {
+ const { tokenValueContainer, tokenValueElement } = findElements(missingLabelToken);
+ const tokenValue = tokenValueElement.innerText;
+ const matchingLabel = findLabel(tokenValue);
+ expect(matchingLabel).toBe(undefined);
+
+ subject.updateLabelTokenColor(tokenValueContainer, tokenValue)
+ .then(() => {
+ expect(tokenValueContainer.getAttribute('style')).toBe(null);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
});
diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb
index 88e3f860809..1a30909977e 100644
--- a/spec/javascripts/fixtures/issues.rb
+++ b/spec/javascripts/fixtures/issues.rb
@@ -36,6 +36,17 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
render_issue(example.description, issue)
end
+ it 'issues/issue_list.html.raw' do |example|
+ create(:issue, project: project)
+
+ get :index,
+ namespace_id: project.namespace.to_param,
+ project_id: project
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+
private
def render_issue(fixture_file_name, issue)
diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js b/spec/javascripts/helpers/filtered_search_spec_helper.js
index 0d7092a2357..8933dd5def4 100644
--- a/spec/javascripts/helpers/filtered_search_spec_helper.js
+++ b/spec/javascripts/helpers/filtered_search_spec_helper.js
@@ -30,12 +30,15 @@ export default class FilteredSearchSpecHelper {
`;
}
+ static createSearchVisualToken(name) {
+ const li = document.createElement('li');
+ li.classList.add('js-visual-token', 'filtered-search-term');
+ li.innerHTML = `<div class="name">${name}</div>`;
+ return li;
+ }
+
static createSearchVisualTokenHTML(name) {
- return `
- <li class="js-visual-token filtered-search-term">
- <div class="name">${name}</div>
- </li>
- `;
+ return FilteredSearchSpecHelper.createSearchVisualToken(name).outerHTML;
}
static createInputHTML(placeholder = '', value = '') {
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 8e24168ad71..9c2e8a298c6 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -110,23 +110,23 @@ EOT
end
end
- context 'using a Gitaly::CommitDiffResponse' do
+ context 'using a GitalyClient::Diff' do
let(:diff) do
described_class.new(
- Gitaly::CommitDiffResponse.new(
+ Gitlab::GitalyClient::Diff.new(
to_path: ".gitmodules",
from_path: ".gitmodules",
old_mode: 0100644,
new_mode: 0100644,
from_id: '357406f3075a57708d0163752905cc1576fceacc',
to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0',
- raw_chunks: raw_chunks
+ patch: raw_patch
)
)
end
context 'with a small diff' do
- let(:raw_chunks) { [@raw_diff_hash[:diff]] }
+ let(:raw_patch) { @raw_diff_hash[:diff] }
it 'initializes the diff' do
expect(diff.to_hash).to eq(@raw_diff_hash)
@@ -138,7 +138,7 @@ EOT
end
context 'using a diff that is too large' do
- let(:raw_chunks) { ['a' * 204800] }
+ let(:raw_patch) { 'a' * 204800 }
it 'prunes the diff' do
expect(diff.diff).to be_empty
diff --git a/spec/lib/gitlab/gitaly_client/diff_spec.rb b/spec/lib/gitlab/gitaly_client/diff_spec.rb
new file mode 100644
index 00000000000..2960c9a79ad
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/diff_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::Diff, lib: true do
+ let(:diff_fields) do
+ {
+ to_path: ".gitmodules",
+ from_path: ".gitmodules",
+ old_mode: 0100644,
+ new_mode: 0100644,
+ from_id: '357406f3075a57708d0163752905cc1576fceacc',
+ to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0',
+ patch: 'a' * 100
+ }
+ end
+
+ subject { described_class.new(diff_fields) }
+
+ it { is_expected.to respond_to(:from_path) }
+ it { is_expected.to respond_to(:to_path) }
+ it { is_expected.to respond_to(:old_mode) }
+ it { is_expected.to respond_to(:new_mode) }
+ it { is_expected.to respond_to(:from_id) }
+ it { is_expected.to respond_to(:to_id) }
+ it { is_expected.to respond_to(:patch) }
+
+ describe '#==' do
+ it { expect(subject).to eq(described_class.new(diff_fields)) }
+ it { expect(subject).not_to eq(described_class.new(diff_fields.merge(patch: 'a'))) }
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb b/spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb
new file mode 100644
index 00000000000..07650013052
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::DiffStitcher, lib: true do
+ describe 'enumeration' do
+ it 'combines segregated diff messages together' do
+ diff_1 = OpenStruct.new(
+ to_path: ".gitmodules",
+ from_path: ".gitmodules",
+ old_mode: 0100644,
+ new_mode: 0100644,
+ from_id: '357406f3075a57708d0163752905cc1576fceacc',
+ to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0',
+ patch: 'a' * 100
+ )
+ diff_2 = OpenStruct.new(
+ to_path: ".gitignore",
+ from_path: ".gitignore",
+ old_mode: 0100644,
+ new_mode: 0100644,
+ from_id: '357406f3075a57708d0163752905cc1576fceacc',
+ to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0',
+ patch: 'a' * 200
+ )
+ diff_3 = OpenStruct.new(
+ to_path: "README",
+ from_path: "README",
+ old_mode: 0100644,
+ new_mode: 0100644,
+ from_id: '357406f3075a57708d0163752905cc1576fceacc',
+ to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0',
+ patch: 'a' * 100
+ )
+
+ msg_1 = OpenStruct.new(diff_1.to_h.except(:patch))
+ msg_1.raw_patch_data = diff_1.patch
+ msg_1.end_of_patch = true
+
+ msg_2 = OpenStruct.new(diff_2.to_h.except(:patch))
+ msg_2.raw_patch_data = diff_2.patch[0..100]
+ msg_2.end_of_patch = false
+
+ msg_3 = OpenStruct.new(raw_patch_data: diff_2.patch[101..-1], end_of_patch: true)
+
+ msg_4 = OpenStruct.new(diff_3.to_h.except(:patch))
+ msg_4.raw_patch_data = diff_3.patch
+ msg_4.end_of_patch = true
+
+ diff_msgs = [msg_1, msg_2, msg_3, msg_4]
+
+ expected_diffs = [
+ Gitlab::GitalyClient::Diff.new(diff_1.to_h),
+ Gitlab::GitalyClient::Diff.new(diff_2.to_h),
+ Gitlab::GitalyClient::Diff.new(diff_3.to_h)
+ ]
+
+ expect(described_class.new(diff_msgs).to_a).to eq(expected_diffs)
+ end
+ end
+end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 7c40cfd8253..f1e2a2cc518 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -66,14 +66,16 @@ describe Key, models: true do
end
it "does not accept the exact same key twice" do
- create(:key, user: user)
- expect(build(:key, user: user)).not_to be_valid
+ first_key = create(:key, user: user)
+
+ expect(build(:key, user: user, key: first_key.key)).not_to be_valid
end
it "does not accept a duplicate key with a different comment" do
- create(:key, user: user)
- duplicate = build(:key, user: user)
+ first_key = create(:key, user: user)
+ duplicate = build(:key, user: user, key: first_key.key)
duplicate.key << ' extra comment'
+
expect(duplicate).not_to be_valid
end
end
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index bc591b2eb37..47cca4275af 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -165,7 +165,7 @@ describe API::V3::Projects do
expect(json_response).to satisfy do |response|
response.one? do |entry|
- entry.has_key?('permissions') &&
+ entry.key?('permissions') &&
entry['name'] == project.name &&
entry['owner']['username'] == user.username
end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index f2426db6d81..088f24eb180 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -113,7 +113,7 @@ describe PipelineSerializer do
it "verifies number of queries" do
recorded = ActiveRecord::QueryRecorder.new { subject }
- expect(recorded.count).to be_within(1).of(58)
+ expect(recorded.count).to be_within(1).of(60)
expect(recorded.cached_count).to eq(0)
end
diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
index 19e8d5cc5f1..c77e6e9cd50 100644
--- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
@@ -26,6 +26,10 @@ describe MergeRequests::Conflicts::ResolveService do
describe '#execute' do
let(:service) { described_class.new(merge_request) }
+ def blob_content(project, ref, path)
+ project.repository.blob_at(ref, path).data
+ end
+
context 'with section params' do
let(:params) do
{
@@ -66,6 +70,35 @@ describe MergeRequests::Conflicts::ResolveService do
end
end
+ context 'when some files have trailing newlines' do
+ let!(:source_head) do
+ branch = 'conflict-resolvable'
+ path = 'files/ruby/popen.rb'
+ popen_content = blob_content(project, branch, path)
+
+ project.repository.update_file(
+ user,
+ path,
+ popen_content.chomp("\n"),
+ message: 'Remove trailing newline from popen.rb',
+ branch_name: branch
+ )
+ end
+
+ before do
+ service.execute(user, params)
+ end
+
+ it 'preserves trailing newlines from our side of the conflicts' do
+ head_sha = merge_request.source_branch_head.sha
+ popen_content = blob_content(project, head_sha, 'files/ruby/popen.rb')
+ regex_content = blob_content(project, head_sha, 'files/ruby/regex.rb')
+
+ expect(popen_content).not_to end_with("\n")
+ expect(regex_content).to end_with("\n")
+ end
+ end
+
context 'when the source project is a fork and does not contain the HEAD of the target branch' do
let!(:target_head) do
project.repository.create_file(
@@ -142,10 +175,13 @@ describe MergeRequests::Conflicts::ResolveService do
end
it 'sets the content to the content given' do
- blob = merge_request.source_project.repository.blob_at(merge_request.source_branch_head.sha,
- 'files/ruby/popen.rb')
+ blob = blob_content(
+ merge_request.source_project,
+ merge_request.source_branch_head.sha,
+ 'files/ruby/popen.rb'
+ )
- expect(blob.data).to eq(popen_content)
+ expect(blob).to eq(popen_content)
end
end
diff --git a/spec/services/wiki_pages/create_service_spec.rb b/spec/services/wiki_pages/create_service_spec.rb
index 5341ba3d261..054e28ae7b0 100644
--- a/spec/services/wiki_pages/create_service_spec.rb
+++ b/spec/services/wiki_pages/create_service_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe WikiPages::CreateService, services: true do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
+
let(:opts) do
{
title: 'Title',
@@ -10,27 +11,28 @@ describe WikiPages::CreateService, services: true do
format: 'markdown'
}
end
- let(:service) { described_class.new(project, user, opts) }
+
+ subject(:service) { described_class.new(project, user, opts) }
+
+ before do
+ project.add_developer(user)
+ end
describe '#execute' do
- context "valid params" do
- before do
- allow(service).to receive(:execute_hooks)
- project.add_master(user)
- end
-
- subject { service.execute }
-
- it 'creates a valid wiki page' do
- is_expected.to be_valid
- expect(subject.title).to eq(opts[:title])
- expect(subject.content).to eq(opts[:content])
- expect(subject.format).to eq(opts[:format].to_sym)
- end
-
- it 'executes webhooks' do
- expect(service).to have_received(:execute_hooks).once.with(subject, 'create')
- end
+ it 'creates wiki page with valid attributes' do
+ page = service.execute
+
+ expect(page).to be_valid
+ expect(page.title).to eq(opts[:title])
+ expect(page.content).to eq(opts[:content])
+ expect(page.format).to eq(opts[:format].to_sym)
+ end
+
+ it 'executes webhooks' do
+ expect(service).to receive(:execute_hooks).once
+ .with(instance_of(WikiPage), 'create')
+
+ service.execute
end
end
end
diff --git a/spec/services/wiki_pages/destroy_service_spec.rb b/spec/services/wiki_pages/destroy_service_spec.rb
index a4b9a390fe2..920be4d4c8a 100644
--- a/spec/services/wiki_pages/destroy_service_spec.rb
+++ b/spec/services/wiki_pages/destroy_service_spec.rb
@@ -3,19 +3,20 @@ require 'spec_helper'
describe WikiPages::DestroyService, services: true do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
- let(:wiki_page) { create(:wiki_page) }
- let(:service) { described_class.new(project, user) }
+ let(:page) { create(:wiki_page) }
- describe '#execute' do
- before do
- allow(service).to receive(:execute_hooks)
- project.add_master(user)
- end
+ subject(:service) { described_class.new(project, user) }
+ before do
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
it 'executes webhooks' do
- service.execute(wiki_page)
+ expect(service).to receive(:execute_hooks).once
+ .with(instance_of(WikiPage), 'delete')
- expect(service).to have_received(:execute_hooks).once.with(wiki_page, 'delete')
+ service.execute(page)
end
end
end
diff --git a/spec/services/wiki_pages/update_service_spec.rb b/spec/services/wiki_pages/update_service_spec.rb
index 2bccca764d7..5e36ea4cf94 100644
--- a/spec/services/wiki_pages/update_service_spec.rb
+++ b/spec/services/wiki_pages/update_service_spec.rb
@@ -3,7 +3,8 @@ require 'spec_helper'
describe WikiPages::UpdateService, services: true do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
- let(:wiki_page) { create(:wiki_page) }
+ let(:page) { create(:wiki_page) }
+
let(:opts) do
{
content: 'New content for wiki page',
@@ -11,27 +12,28 @@ describe WikiPages::UpdateService, services: true do
message: 'New wiki message'
}
end
- let(:service) { described_class.new(project, user, opts) }
+
+ subject(:service) { described_class.new(project, user, opts) }
+
+ before do
+ project.add_developer(user)
+ end
describe '#execute' do
- context "valid params" do
- before do
- allow(service).to receive(:execute_hooks)
- project.add_master(user)
- end
-
- subject { service.execute(wiki_page) }
-
- it 'updates the wiki page' do
- is_expected.to be_valid
- expect(subject.content).to eq(opts[:content])
- expect(subject.format).to eq(opts[:format].to_sym)
- expect(subject.message).to eq(opts[:message])
- end
-
- it 'executes webhooks' do
- expect(service).to have_received(:execute_hooks).once.with(subject, 'update')
- end
+ it 'updates the wiki page' do
+ updated_page = service.execute(page)
+
+ expect(updated_page).to be_valid
+ expect(updated_page.message).to eq(opts[:message])
+ expect(updated_page.content).to eq(opts[:content])
+ expect(updated_page.format).to eq(opts[:format].to_sym)
+ end
+
+ it 'executes webhooks' do
+ expect(service).to receive(:execute_hooks).once
+ .with(instance_of(WikiPage), 'update')
+
+ service.execute(page)
end
end
end
diff --git a/spec/support/helpers/key_generator_helper.rb b/spec/support/helpers/key_generator_helper.rb
new file mode 100644
index 00000000000..b1c289ffef7
--- /dev/null
+++ b/spec/support/helpers/key_generator_helper.rb
@@ -0,0 +1,41 @@
+module Spec
+ module Support
+ module Helpers
+ class KeyGeneratorHelper
+ # The components in a openssh .pub / known_host RSA public key.
+ RSA_COMPONENTS = ['ssh-rsa', :e, :n].freeze
+
+ attr_reader :size
+
+ def initialize(size = 2048)
+ @size = size
+ end
+
+ def generate
+ key = OpenSSL::PKey::RSA.generate(size)
+ components = RSA_COMPONENTS.map do |component|
+ key.respond_to?(component) ? encode_mpi(key.public_send(component)) : component
+ end
+
+ # Ruby tries to be helpful and adds new lines every 60 bytes :(
+ 'ssh-rsa ' + [pack_pubkey_components(components)].pack('m').delete("\n")
+ end
+
+ private
+
+ # Encodes an openssh-mpi-encoded integer.
+ def encode_mpi(n)
+ chars, n = [], n.to_i
+ chars << (n & 0xff) && n >>= 8 while n != 0
+ chars << 0 if chars.empty? || chars.last >= 0x80
+ chars.reverse.pack('C*')
+ end
+
+ # Packs string components into an openssh-encoded pubkey.
+ def pack_pubkey_components(strings)
+ (strings.map { |s| [s.length].pack('N') }).zip(strings).flatten.join
+ end
+ end
+ end
+ end
+end