Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--README.md1
-rw-r--r--app/assets/javascripts/project.js4
-rw-r--r--app/assets/javascripts/project_select.js42
-rw-r--r--app/helpers/projects_helper.rb6
-rw-r--r--app/models/concerns/issuable.rb11
-rw-r--r--app/views/layouts/project.html.haml8
-rw-r--r--changelogs/unreleased/35010-remove-goto-project-from-breadcrumb.yml5
-rw-r--r--changelogs/unreleased/api-gpg-key-management.yml5
-rw-r--r--changelogs/unreleased/feature-dependency-status-badge.yml5
-rw-r--r--changelogs/unreleased/fuzzy-issue-search.yml5
-rw-r--r--doc/api/users.md211
-rw-r--r--doc/user/search/img/issue_search_by_term.pngbin0 -> 65826 bytes
-rw-r--r--doc/user/search/index.md14
-rw-r--r--lib/api/entities.rb4
-rw-r--r--lib/api/users.rb150
-rw-r--r--lib/gitlab/git/repository.rb18
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb14
-rw-r--r--lib/gitlab/sql/pattern.rb23
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb4
-rw-r--r--spec/features/boards/boards_spec.rb18
-rw-r--r--spec/features/projects_spec.rb43
-rw-r--r--spec/javascripts/project_title_spec.js60
-rw-r--r--spec/javascripts/projects_dropdown/service/projects_service_spec.js3
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb38
-rw-r--r--spec/lib/gitlab/sql/pattern_spec.rb120
-rw-r--r--spec/models/concerns/issuable_spec.rb42
-rw-r--r--spec/requests/api/users_spec.rb326
31 files changed, 974 insertions, 216 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b08c441028b..778d33fb960 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -208,7 +208,7 @@ update-tests-metadata:
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
- - rm -f rspec_flaky/${CI_PROJECT_NAME}/all_node_*.json
+ - rm -f rspec_flaky/${CI_PROJECT_NAME}/*_node_*.json
flaky-examples-check:
<<: *dedicated-runner
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 93d4c1ef06f..0f1a7dfc7c4 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.36.0
+0.37.0
diff --git a/Gemfile b/Gemfile
index 5b551005c6e..0341f2609ad 100644
--- a/Gemfile
+++ b/Gemfile
@@ -397,7 +397,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.31.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.32.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 55653e72c24..320d42b8974 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -275,7 +275,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.31.0)
+ gitaly-proto (0.32.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -1021,7 +1021,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
- gitaly-proto (~> 0.31.0)
+ gitaly-proto (~> 0.32.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
diff --git a/README.md b/README.md
index 9309922ae39..9ead6d51c5d 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,7 @@
[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines)
+[![Dependency Status](https://gemnasium.com/gitlabhq/gitlabhq.svg)](https://gemnasium.com/gitlabhq/gitlabhq)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
[![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index d7e3ab42f00..fe6602259e2 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -53,10 +53,6 @@ import Cookies from 'js-cookie';
return _this.changeProject($(e.currentTarget).val());
};
})(this));
- return $('.js-projects-dropdown-toggle').on('click', function(e) {
- e.preventDefault();
- return $('.js-projects-dropdown').select2('open');
- });
};
Project.prototype.changeProject = function(url) {
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 1b4ed6be90a..fb01390f91c 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -5,48 +5,6 @@ import ProjectSelectComboButton from './project_select_combo_button';
(function() {
this.ProjectSelect = (function() {
function ProjectSelect() {
- $('.js-projects-dropdown-toggle').each(function(i, dropdown) {
- var $dropdown;
- $dropdown = $(dropdown);
- return $dropdown.glDropdown({
- filterable: true,
- filterRemote: true,
- search: {
- fields: ['name_with_namespace']
- },
- data: function(term, callback) {
- var finalCallback, projectsCallback;
- var orderBy = $dropdown.data('order-by');
- finalCallback = function(projects) {
- return callback(projects);
- };
- if (this.includeGroups) {
- projectsCallback = function(projects) {
- var groupsCallback;
- groupsCallback = function(groups) {
- var data;
- data = groups.concat(projects);
- return finalCallback(data);
- };
- return Api.groups(term, {}, groupsCallback);
- };
- } else {
- projectsCallback = finalCallback;
- }
- if (this.groupId) {
- return Api.groupProjects(this.groupId, term, projectsCallback);
- } else {
- return Api.projects(term, { order_by: orderBy }, projectsCallback);
- }
- },
- url: function(project) {
- return project.web_url;
- },
- text: function(project) {
- return project.name_with_namespace;
- }
- });
- });
$('.ajax-project-select').each(function(i, select) {
var placeholder;
this.groupId = $(select).data('group-id');
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 0bf94fd30db..02fe82ea872 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -72,12 +72,6 @@ module ProjectsHelper
output.html_safe
end
- if current_user
- project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do
- icon("chevron-down")
- end
- end
-
"#{namespace_link} / #{project_link}".html_safe
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 3731b7c8577..681c3241dbb 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -6,6 +6,7 @@
#
module Issuable
extend ActiveSupport::Concern
+ include Gitlab::SQL::Pattern
include CacheMarkdownField
include Participable
include Mentionable
@@ -122,7 +123,9 @@ module Issuable
#
# Returns an ActiveRecord::Relation.
def search(query)
- where(arel_table[:title].matches("%#{query}%"))
+ title = to_fuzzy_arel(:title, query)
+
+ where(title)
end
# Searches for records with a matching title or description.
@@ -133,10 +136,10 @@ module Issuable
#
# Returns an ActiveRecord::Relation.
def full_search(query)
- t = arel_table
- pattern = "%#{query}%"
+ title = to_fuzzy_arel(:title, query)
+ description = to_fuzzy_arel(:description, query)
- where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
+ where(title&.or(description))
end
def sort(method, excluded_labels: [])
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 54d56e9b873..d6db85ee87a 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -14,12 +14,4 @@
:javascript
window.uploads_path = "#{project_uploads_path(project)}";
-- content_for :header_content do
- .js-dropdown-menu-projects
- .dropdown-menu.dropdown-select.dropdown-menu-projects
- = dropdown_title("Go to a project")
- = dropdown_filter("Search your projects")
- = dropdown_content
- = dropdown_loading
-
= render template: "layouts/application"
diff --git a/changelogs/unreleased/35010-remove-goto-project-from-breadcrumb.yml b/changelogs/unreleased/35010-remove-goto-project-from-breadcrumb.yml
new file mode 100644
index 00000000000..6cd7f4e9cc6
--- /dev/null
+++ b/changelogs/unreleased/35010-remove-goto-project-from-breadcrumb.yml
@@ -0,0 +1,5 @@
+---
+title: Remove project select dropdown from breadcrumb
+merge_request: 14010
+author:
+type: changed
diff --git a/changelogs/unreleased/api-gpg-key-management.yml b/changelogs/unreleased/api-gpg-key-management.yml
new file mode 100644
index 00000000000..0be35a5823b
--- /dev/null
+++ b/changelogs/unreleased/api-gpg-key-management.yml
@@ -0,0 +1,5 @@
+---
+title: 'API: Add GPG key management'
+merge_request: 13828
+author: Robert Schilling
+type: added
diff --git a/changelogs/unreleased/feature-dependency-status-badge.yml b/changelogs/unreleased/feature-dependency-status-badge.yml
new file mode 100644
index 00000000000..1becff3585a
--- /dev/null
+++ b/changelogs/unreleased/feature-dependency-status-badge.yml
@@ -0,0 +1,5 @@
+---
+title: Add badge for dependency status
+merge_request: 13588
+author: Markus Koller
+type: other
diff --git a/changelogs/unreleased/fuzzy-issue-search.yml b/changelogs/unreleased/fuzzy-issue-search.yml
new file mode 100644
index 00000000000..8195e97ed59
--- /dev/null
+++ b/changelogs/unreleased/fuzzy-issue-search.yml
@@ -0,0 +1,5 @@
+---
+title: Support a multi-word fuzzy seach issues/merge requests on search bar
+merge_request: 13780
+author: Hiroyuki Sato
+type: changed
diff --git a/doc/api/users.md b/doc/api/users.md
index 57a13eb477d..57b4e117cf3 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -550,6 +550,217 @@ Parameters:
Will return `200 OK` on success, or `404 Not found` if either user or key cannot be found.
+## List all GPG keys
+
+Get a list of currently authenticated user's GPG keys.
+
+```
+GET /user/gpg_keys
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "created_at": "2017-09-05T09:17:46.264Z"
+ }
+]
+```
+
+## Get a specific GPG key
+
+Get a specific GPG key of currently authenticated user.
+
+```
+GET /user/gpg_keys/:key_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `key_id` | integer | yes | The ID of the GPG key |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys/1
+```
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "created_at": "2017-09-05T09:17:46.264Z"
+ }
+```
+
+## Add a GPG key
+
+Creates a new GPG key owned by the currently authenticated user.
+
+```
+POST /user/gpg_keys
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| key | string | yes | The new GPG key |
+
+```bash
+curl --data "key=-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFV..." --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "created_at": "2017-09-05T09:17:46.264Z"
+ }
+]
+```
+
+## Delete a GPG key
+
+Delete a GPG key owned by currently authenticated user.
+
+```
+DELETE /user/gpg_keys/:key_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `key_id` | integer | yes | The ID of the GPG key |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys/1
+```
+
+Returns `204 No Content` on success, or `404 Not found` if the key cannot be found.
+
+## List all GPG keys for given user
+
+Get a list of a specified user's GPG keys. Available only for admins.
+
+```
+GET /users/:id/gpg_keys
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "created_at": "2017-09-05T09:17:46.264Z"
+ }
+]
+```
+
+## Get a specific GPG key for a given user
+
+Get a specific GPG key for a given user. Available only for admins.
+
+```
+GET /users/:id/gpg_keys/:key_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
+| `key_id` | integer | yes | The ID of the GPG key |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys/1
+```
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "created_at": "2017-09-05T09:17:46.264Z"
+ }
+```
+
+## Add a GPG key for a given user
+
+Create new GPG key owned by the specified user. Available only for admins.
+
+```
+POST /users/:id/gpg_keys
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
+| `key_id` | integer | yes | The ID of the GPG key |
+
+```bash
+curl --data "key=-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFV..." --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "created_at": "2017-09-05T09:17:46.264Z"
+ }
+]
+```
+
+## Delete a GPG key for a given user
+
+Delete a GPG key owned by a specified user. Available only for admins.
+
+```
+DELETE /users/:id/gpg_keys/:key_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
+| `key_id` | integer | yes | The ID of the GPG key |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys/1
+```
+
## List emails
Get a list of currently authenticated user's emails.
diff --git a/doc/user/search/img/issue_search_by_term.png b/doc/user/search/img/issue_search_by_term.png
new file mode 100644
index 00000000000..66e612c4ea6
--- /dev/null
+++ b/doc/user/search/img/issue_search_by_term.png
Binary files differ
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index f5c7ce49e8e..21e96d8b11c 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -40,6 +40,20 @@ The same process is valid for merge requests. Navigate to your project's **Merge
and click **Search or filter results...**. Merge requests can be filtered by author, assignee,
milestone, and label.
+### Searching for specific terms
+
+You can filter issues and merge requests by specific terms included in titles or descriptions.
+
+* Syntax
+ * Searches look for all the words in a query, in any order. E.g.: searching
+ issues for `display bug` will return all issues matching both those words, in any order.
+ * To find the exact term, use double quotes: `"display bug"`
+* Limitation
+ * For performance reasons, terms shorter than 3 chars are ignored. E.g.: searching
+ issues for `included in titles` is same as `included titles`
+
+![filter issues by specific terms](img/issue_search_by_term.png)
+
### Issues and merge requests per group
Similar to **Issues and merge requests per project**, you can also search for issues
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 0092cc14e74..031dd02c6eb 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -491,6 +491,10 @@ module API
expose :user, using: Entities::UserPublic
end
+ class GPGKey < Grape::Entity
+ expose :id, :key, :created_at
+ end
+
class Note < Grape::Entity
# Only Issue and MergeRequest have iid
NOTEABLE_TYPES_WITH_IID = %w(Issue MergeRequest).freeze
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 96f47bb618a..1825c90a23b 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -233,6 +233,86 @@ module API
destroy_conditionally!(key)
end
+ desc 'Add a GPG key to a specified user. Available only for admins.' do
+ detail 'This feature was added in GitLab 10.0'
+ success Entities::GPGKey
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :key, type: String, desc: 'The new GPG key'
+ end
+ post ':id/gpg_keys' do
+ authenticated_as_admin!
+
+ user = User.find_by(id: params.delete(:id))
+ not_found!('User') unless user
+
+ key = user.gpg_keys.new(declared_params(include_missing: false))
+
+ if key.save
+ present key, with: Entities::GPGKey
+ else
+ render_validation_error!(key)
+ end
+ end
+
+ desc 'Get the GPG keys of a specified user. Available only for admins.' do
+ detail 'This feature was added in GitLab 10.0'
+ success Entities::GPGKey
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ use :pagination
+ end
+ get ':id/gpg_keys' do
+ authenticated_as_admin!
+
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ present paginate(user.gpg_keys), with: Entities::GPGKey
+ end
+
+ desc 'Delete an existing GPG key from a specified user. Available only for admins.' do
+ detail 'This feature was added in GitLab 10.0'
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :key_id, type: Integer, desc: 'The ID of the GPG key'
+ end
+ delete ':id/gpg_keys/:key_id' do
+ authenticated_as_admin!
+
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ key = user.gpg_keys.find_by(id: params[:key_id])
+ not_found!('GPG Key') unless key
+
+ status 204
+ key.destroy
+ end
+
+ desc 'Revokes an existing GPG key from a specified user. Available only for admins.' do
+ detail 'This feature was added in GitLab 10.0'
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :key_id, type: Integer, desc: 'The ID of the GPG key'
+ end
+ post ':id/gpg_keys/:key_id/revoke' do
+ authenticated_as_admin!
+
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ key = user.gpg_keys.find_by(id: params[:key_id])
+ not_found!('GPG Key') unless key
+
+ key.revoke
+ status :accepted
+ end
+
desc 'Add an email address to a specified user. Available only for admins.' do
success Entities::Email
end
@@ -492,6 +572,76 @@ module API
destroy_conditionally!(key)
end
+ desc "Get the currently authenticated user's GPG keys" do
+ detail 'This feature was added in GitLab 10.0'
+ success Entities::GPGKey
+ end
+ params do
+ use :pagination
+ end
+ get 'gpg_keys' do
+ present paginate(current_user.gpg_keys), with: Entities::GPGKey
+ end
+
+ desc 'Get a single GPG key owned by currently authenticated user' do
+ detail 'This feature was added in GitLab 10.0'
+ success Entities::GPGKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the GPG key'
+ end
+ get 'gpg_keys/:key_id' do
+ key = current_user.gpg_keys.find_by(id: params[:key_id])
+ not_found!('GPG Key') unless key
+
+ present key, with: Entities::GPGKey
+ end
+
+ desc 'Add a new GPG key to the currently authenticated user' do
+ detail 'This feature was added in GitLab 10.0'
+ success Entities::GPGKey
+ end
+ params do
+ requires :key, type: String, desc: 'The new GPG key'
+ end
+ post 'gpg_keys' do
+ key = current_user.gpg_keys.new(declared_params)
+
+ if key.save
+ present key, with: Entities::GPGKey
+ else
+ render_validation_error!(key)
+ end
+ end
+
+ desc 'Revoke a GPG key owned by currently authenticated user' do
+ detail 'This feature was added in GitLab 10.0'
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the GPG key'
+ end
+ post 'gpg_keys/:key_id/revoke' do
+ key = current_user.gpg_keys.find_by(id: params[:key_id])
+ not_found!('GPG Key') unless key
+
+ key.revoke
+ status :accepted
+ end
+
+ desc 'Delete a GPG key from the currently authenticated user' do
+ detail 'This feature was added in GitLab 10.0'
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the SSH key'
+ end
+ delete 'gpg_keys/:key_id' do
+ key = current_user.gpg_keys.find_by(id: params[:key_id])
+ not_found!('GPG Key') unless key
+
+ status 204
+ key.destroy
+ end
+
desc "Get the currently authenticated user's email addresses" do
success Entities::Email
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index f7577b02d5d..75d4efc0bc5 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -134,15 +134,19 @@ module Gitlab
# This is to work around a bug in libgit2 that causes in-memory refs to
# be stale/invalid when packed-refs is changed.
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/15392#note_14538333
- #
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/474
def find_branch(name, force_reload = false)
- reload_rugged if force_reload
+ gitaly_migrate(:find_branch) do |is_enabled|
+ if is_enabled
+ gitaly_ref_client.find_branch(name)
+ else
+ reload_rugged if force_reload
- rugged_ref = rugged.branches[name]
- if rugged_ref
- target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
- Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
+ rugged_ref = rugged.branches[name]
+ if rugged_ref
+ target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
+ Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
+ end
+ end
end
end
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index 8c0008c6971..a1a25cf2079 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -78,6 +78,20 @@ module Gitlab
raise ArgumentError, e.message
end
+ def find_branch(branch_name)
+ request = Gitaly::DeleteBranchRequest.new(
+ repository: @gitaly_repo,
+ name: GitalyClient.encode(branch_name)
+ )
+
+ response = GitalyClient.call(@repository.storage, :ref_service, :find_branch, request)
+ branch = response.branch
+ return unless branch
+
+ target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit)
+ Gitlab::Git::Branch.new(@repository, encode!(branch.name.dup), branch.target_commit.id, target_commit)
+ end
+
private
def consume_refs_response(response)
diff --git a/lib/gitlab/sql/pattern.rb b/lib/gitlab/sql/pattern.rb
index b42bc67ccfc..7c2d1d8f887 100644
--- a/lib/gitlab/sql/pattern.rb
+++ b/lib/gitlab/sql/pattern.rb
@@ -4,6 +4,7 @@ module Gitlab
extend ActiveSupport::Concern
MIN_CHARS_FOR_PARTIAL_MATCHING = 3
+ REGEX_QUOTED_WORD = /(?<=^| )"[^"]+"(?= |$)/
class_methods do
def to_pattern(query)
@@ -17,6 +18,28 @@ module Gitlab
def partial_matching?(query)
query.length >= MIN_CHARS_FOR_PARTIAL_MATCHING
end
+
+ def to_fuzzy_arel(column, query)
+ words = select_fuzzy_words(query)
+
+ matches = words.map { |word| arel_table[column].matches(to_pattern(word)) }
+
+ matches.reduce { |result, match| result.and(match) }
+ end
+
+ def select_fuzzy_words(query)
+ quoted_words = query.scan(REGEX_QUOTED_WORD)
+
+ query = quoted_words.reduce(query) { |q, quoted_word| q.sub(quoted_word, '') }
+
+ words = query.split(/\s+/)
+
+ quoted_words.map! { |quoted_word| quoted_word[1..-2] }
+
+ words.concat(quoted_words)
+
+ words.select { |word| partial_matching?(word) }
+ end
end
end
end
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index a6ad5981f8f..c480b5b7e34 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -8,8 +8,8 @@ describe 'Issue Boards add issue modal', :js do
let!(:label) { create(:label, project: project) }
let!(:list1) { create(:list, board: board, label: planning, position: 0) }
let!(:list2) { create(:list, board: board, label: label, position: 1) }
- let!(:issue) { create(:issue, project: project) }
- let!(:issue2) { create(:issue, project: project) }
+ let!(:issue) { create(:issue, project: project, title: 'abc', description: 'def') }
+ let!(:issue2) { create(:issue, project: project, title: 'hij', description: 'klm') }
before do
project.team << [user, :master]
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 913258ca40f..e010b5f3444 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -73,15 +73,15 @@ describe 'Issue Boards', js: true do
let!(:list2) { create(:list, board: board, label: development, position: 1) }
let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
- let!(:issue1) { create(:labeled_issue, project: project, assignees: [user], labels: [planning], relative_position: 8) }
- let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning], relative_position: 7) }
- let!(:issue3) { create(:labeled_issue, project: project, labels: [planning], relative_position: 6) }
- let!(:issue4) { create(:labeled_issue, project: project, labels: [planning], relative_position: 5) }
- let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone, relative_position: 4) }
- let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development], relative_position: 3) }
- let!(:issue7) { create(:labeled_issue, project: project, labels: [development], relative_position: 2) }
- let!(:issue8) { create(:closed_issue, project: project) }
- let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting], relative_position: 1) }
+ let!(:issue1) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) }
+ let!(:issue2) { create(:labeled_issue, project: project, title: 'bbb', description: '222', author: user2, labels: [planning], relative_position: 7) }
+ let!(:issue3) { create(:labeled_issue, project: project, title: 'ccc', description: '333', labels: [planning], relative_position: 6) }
+ let!(:issue4) { create(:labeled_issue, project: project, title: 'ddd', description: '444', labels: [planning], relative_position: 5) }
+ let!(:issue5) { create(:labeled_issue, project: project, title: 'eee', description: '555', labels: [planning], milestone: milestone, relative_position: 4) }
+ let!(:issue6) { create(:labeled_issue, project: project, title: 'fff', description: '666', labels: [planning, development], relative_position: 3) }
+ let!(:issue7) { create(:labeled_issue, project: project, title: 'ggg', description: '777', labels: [development], relative_position: 2) }
+ let!(:issue8) { create(:closed_issue, project: project, title: 'hhh', description: '888') }
+ let!(:issue9) { create(:labeled_issue, project: project, title: 'iii', description: '999', labels: [planning, testing, bug, accepting], relative_position: 1) }
before do
visit project_board_path(project, board)
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index baf3d29e6c5..81f7ab80a04 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -95,49 +95,6 @@ feature 'Project' do
end
end
- describe 'project title' do
- let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
-
- before do
- sign_in(user)
- project.add_user(user, Gitlab::Access::MASTER)
- visit project_path(project)
- end
-
- it 'clicks toggle and shows dropdown', js: true do
- find('.js-projects-dropdown-toggle').click
- expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 1)
- end
- end
-
- describe 'project title' do
- let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
- let(:project2) { create(:project, namespace: user.namespace, path: 'test') }
- let(:issue) { create(:issue, project: project) }
-
- context 'on issues page', js: true do
- before do
- sign_in(user)
- project.add_user(user, Gitlab::Access::MASTER)
- project2.add_user(user, Gitlab::Access::MASTER)
- visit project_issue_path(project, issue)
- end
-
- it 'clicks toggle and shows dropdown' do
- find('.js-projects-dropdown-toggle').click
- expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 2)
-
- page.within '.dropdown-menu-projects' do
- click_link project.name_with_namespace
- end
-
- expect(page).to have_content project.name
- end
- end
- end
-
describe 'tree view (default view is set to Files)' do
let(:user) { create(:user, project_view: 'files') }
let(:project) { create(:forked_project_with_submodules) }
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
deleted file mode 100644
index 9ea8414ae40..00000000000
--- a/spec/javascripts/project_title_spec.js
+++ /dev/null
@@ -1,60 +0,0 @@
-/* global Project */
-
-import 'select2/select2';
-import '~/gl_dropdown';
-import '~/api';
-import '~/project_select';
-import '~/project';
-
-describe('Project Title', () => {
- const dummyApiVersion = 'v3000';
- preloadFixtures('issues/open-issue.html.raw');
- loadJSONFixtures('projects.json');
-
- beforeEach(() => {
- loadFixtures('issues/open-issue.html.raw');
-
- window.gon = {};
- window.gon.api_version = dummyApiVersion;
-
- // eslint-disable-next-line no-new
- new Project();
- });
-
- describe('project list', () => {
- let reqUrl;
- let reqData;
-
- beforeEach(() => {
- const fakeResponseData = getJSONFixture('projects.json');
- spyOn(jQuery, 'ajax').and.callFake((req) => {
- const def = $.Deferred();
- reqUrl = req.url;
- reqData = req.data;
- def.resolve(fakeResponseData);
- return def.promise();
- });
- });
-
- it('toggles dropdown', () => {
- const $menu = $('.js-dropdown-menu-projects');
- window.gon.current_user_id = 1;
- $('.js-projects-dropdown-toggle').click();
- expect($menu).toHaveClass('open');
- expect(reqUrl).toBe(`/api/${dummyApiVersion}/projects.json`);
- expect(reqData).toEqual({
- search: '',
- order_by: 'last_activity_at',
- per_page: 20,
- membership: true,
- simple: true,
- });
- $menu.find('.dropdown-menu-close-icon').click();
- expect($menu).not.toHaveClass('open');
- });
- });
-
- afterEach(() => {
- window.gon = {};
- });
-});
diff --git a/spec/javascripts/projects_dropdown/service/projects_service_spec.js b/spec/javascripts/projects_dropdown/service/projects_service_spec.js
index e9aabfaec1f..d5dd8b3449a 100644
--- a/spec/javascripts/projects_dropdown/service/projects_service_spec.js
+++ b/spec/javascripts/projects_dropdown/service/projects_service_spec.js
@@ -15,6 +15,7 @@ describe('ProjectsService', () => {
beforeEach(() => {
gon.api_version = currentSession.apiVersion;
+ gon.current_user_id = 1;
service = new ProjectsService(currentSession.username);
});
@@ -35,7 +36,7 @@ describe('ProjectsService', () => {
const queryParams = {
simple: false,
per_page: 20,
- membership: false,
+ membership: true,
order_by: 'last_activity_at',
search: searchQuery,
};
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 4cfb4b7d357..08959e7bc16 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -916,27 +916,37 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#find_branch' do
- it 'should return a Branch for master' do
- branch = repository.find_branch('master')
+ shared_examples 'finding a branch' do
+ it 'should return a Branch for master' do
+ branch = repository.find_branch('master')
- expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
- expect(branch.name).to eq('master')
- end
+ expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
+ expect(branch.name).to eq('master')
+ end
- it 'should handle non-existent branch' do
- branch = repository.find_branch('this-is-garbage')
+ it 'should handle non-existent branch' do
+ branch = repository.find_branch('this-is-garbage')
- expect(branch).to eq(nil)
+ expect(branch).to eq(nil)
+ end
end
- it 'should reload Rugged::Repository and return master' do
- expect(Rugged::Repository).to receive(:new).twice.and_call_original
+ context 'when Gitaly find_branch feature is enabled' do
+ it_behaves_like 'finding a branch'
+ end
- repository.find_branch('master')
- branch = repository.find_branch('master', force_reload: true)
+ context 'when Gitaly find_branch feature is disabled', skip_gitaly_mock: true do
+ it_behaves_like 'finding a branch'
- expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
- expect(branch.name).to eq('master')
+ it 'should reload Rugged::Repository and return master' do
+ expect(Rugged::Repository).to receive(:new).twice.and_call_original
+
+ repository.find_branch('master')
+ branch = repository.find_branch('master', force_reload: true)
+
+ expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
+ expect(branch.name).to eq('master')
+ end
end
end
diff --git a/spec/lib/gitlab/sql/pattern_spec.rb b/spec/lib/gitlab/sql/pattern_spec.rb
index 9d7b2136dab..48d56628ed5 100644
--- a/spec/lib/gitlab/sql/pattern_spec.rb
+++ b/spec/lib/gitlab/sql/pattern_spec.rb
@@ -52,4 +52,124 @@ describe Gitlab::SQL::Pattern do
end
end
end
+
+ describe '.select_fuzzy_words' do
+ subject(:select_fuzzy_words) { Issue.select_fuzzy_words(query) }
+
+ context 'with a word equal to 3 chars' do
+ let(:query) { 'foo' }
+
+ it 'returns array cotaining a word' do
+ expect(select_fuzzy_words).to match_array(['foo'])
+ end
+ end
+
+ context 'with a word shorter than 3 chars' do
+ let(:query) { 'fo' }
+
+ it 'returns empty array' do
+ expect(select_fuzzy_words).to match_array([])
+ end
+ end
+
+ context 'with two words both equal to 3 chars' do
+ let(:query) { 'foo baz' }
+
+ it 'returns array containing two words' do
+ expect(select_fuzzy_words).to match_array(%w[foo baz])
+ end
+ end
+
+ context 'with two words divided by two spaces both equal to 3 chars' do
+ let(:query) { 'foo baz' }
+
+ it 'returns array containing two words' do
+ expect(select_fuzzy_words).to match_array(%w[foo baz])
+ end
+ end
+
+ context 'with two words equal to 3 chars and shorter than 3 chars' do
+ let(:query) { 'foo ba' }
+
+ it 'returns array containing a word' do
+ expect(select_fuzzy_words).to match_array(['foo'])
+ end
+ end
+
+ context 'with a multi-word surrounded by double quote' do
+ let(:query) { '"really bar"' }
+
+ it 'returns array containing a multi-word' do
+ expect(select_fuzzy_words).to match_array(['really bar'])
+ end
+ end
+
+ context 'with a multi-word surrounded by double quote and two words' do
+ let(:query) { 'foo "really bar" baz' }
+
+ it 'returns array containing a multi-word and tow words' do
+ expect(select_fuzzy_words).to match_array(['foo', 'really bar', 'baz'])
+ end
+ end
+
+ context 'with a multi-word surrounded by double quote missing a spece before the first double quote' do
+ let(:query) { 'foo"really bar"' }
+
+ it 'returns array containing two words with double quote' do
+ expect(select_fuzzy_words).to match_array(['foo"really', 'bar"'])
+ end
+ end
+
+ context 'with a multi-word surrounded by double quote missing a spece after the second double quote' do
+ let(:query) { '"really bar"baz' }
+
+ it 'returns array containing two words with double quote' do
+ expect(select_fuzzy_words).to match_array(['"really', 'bar"baz'])
+ end
+ end
+
+ context 'with two multi-word surrounded by double quote and two words' do
+ let(:query) { 'foo "really bar" baz "awesome feature"' }
+
+ it 'returns array containing two multi-words and tow words' do
+ expect(select_fuzzy_words).to match_array(['foo', 'really bar', 'baz', 'awesome feature'])
+ end
+ end
+ end
+
+ describe '.to_fuzzy_arel' do
+ subject(:to_fuzzy_arel) { Issue.to_fuzzy_arel(:title, query) }
+
+ context 'with a word equal to 3 chars' do
+ let(:query) { 'foo' }
+
+ it 'returns a single ILIKE condition' do
+ expect(to_fuzzy_arel.to_sql).to match(/title.*I?LIKE '\%foo\%'/)
+ end
+ end
+
+ context 'with a word shorter than 3 chars' do
+ let(:query) { 'fo' }
+
+ it 'returns nil' do
+ expect(to_fuzzy_arel).to be_nil
+ end
+ end
+
+ context 'with two words both equal to 3 chars' do
+ let(:query) { 'foo baz' }
+
+ it 'returns a joining LIKE condition using a AND' do
+ expect(to_fuzzy_arel.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%'/)
+ end
+ end
+
+ context 'with a multi-word surrounded by double quote and two words' do
+ let(:query) { 'foo "really bar" baz' }
+
+ it 'returns a joining LIKE condition using a AND' do
+ expect(to_fuzzy_arel.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%' AND .*title.*I?LIKE '\%really bar\%'/)
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index dfbe1a7c192..37f6fd3a25b 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -66,56 +66,76 @@ describe Issuable do
end
describe ".search" do
- let!(:searchable_issue) { create(:issue, title: "Searchable issue") }
+ let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") }
- it 'returns notes with a matching title' do
+ it 'returns issues with a matching title' do
expect(issuable_class.search(searchable_issue.title))
.to eq([searchable_issue])
end
- it 'returns notes with a partially matching title' do
+ it 'returns issues with a partially matching title' do
expect(issuable_class.search('able')).to eq([searchable_issue])
end
- it 'returns notes with a matching title regardless of the casing' do
+ it 'returns issues with a matching title regardless of the casing' do
expect(issuable_class.search(searchable_issue.title.upcase))
.to eq([searchable_issue])
end
+
+ it 'returns issues with a fuzzy matching title' do
+ expect(issuable_class.search('searchable issue')).to eq([searchable_issue])
+ end
+
+ it 'returns all issues with a query shorter than 3 chars' do
+ expect(issuable_class.search('zz')).to eq(issuable_class.all)
+ end
end
describe ".full_search" do
let!(:searchable_issue) do
- create(:issue, title: "Searchable issue", description: 'kittens')
+ create(:issue, title: "Searchable awesome issue", description: 'Many cute kittens')
end
- it 'returns notes with a matching title' do
+ it 'returns issues with a matching title' do
expect(issuable_class.full_search(searchable_issue.title))
.to eq([searchable_issue])
end
- it 'returns notes with a partially matching title' do
+ it 'returns issues with a partially matching title' do
expect(issuable_class.full_search('able')).to eq([searchable_issue])
end
- it 'returns notes with a matching title regardless of the casing' do
+ it 'returns issues with a matching title regardless of the casing' do
expect(issuable_class.full_search(searchable_issue.title.upcase))
.to eq([searchable_issue])
end
- it 'returns notes with a matching description' do
+ it 'returns issues with a fuzzy matching title' do
+ expect(issuable_class.full_search('searchable issue')).to eq([searchable_issue])
+ end
+
+ it 'returns issues with a matching description' do
expect(issuable_class.full_search(searchable_issue.description))
.to eq([searchable_issue])
end
- it 'returns notes with a partially matching description' do
+ it 'returns issues with a partially matching description' do
expect(issuable_class.full_search(searchable_issue.description))
.to eq([searchable_issue])
end
- it 'returns notes with a matching description regardless of the casing' do
+ it 'returns issues with a matching description regardless of the casing' do
expect(issuable_class.full_search(searchable_issue.description.upcase))
.to eq([searchable_issue])
end
+
+ it 'returns issues with a fuzzy matching description' do
+ expect(issuable_class.full_search('many kittens')).to eq([searchable_issue])
+ end
+
+ it 'returns all issues with a query shorter than 3 chars' do
+ expect(issuable_class.search('zz')).to eq(issuable_class.all)
+ end
end
describe '.to_ability_name' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 5fef4437997..37cb95a16e3 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -4,6 +4,7 @@ describe API::Users do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) }
+ let(:gpg_key) { create(:gpg_key, user: user) }
let(:email) { create(:email, user: user) }
let(:omniauth_user) { create(:omniauth_user) }
let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') }
@@ -753,6 +754,164 @@ describe API::Users do
end
end
+ describe 'POST /users/:id/keys' do
+ before do
+ admin
+ end
+
+ it 'does not create invalid GPG key' do
+ post api("/users/#{user.id}/gpg_keys", admin)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('key is missing')
+ end
+
+ it 'creates GPG key' do
+ key_attrs = attributes_for :gpg_key
+ expect do
+ post api("/users/#{user.id}/gpg_keys", admin), key_attrs
+
+ expect(response).to have_http_status(201)
+ end.to change { user.gpg_keys.count }.by(1)
+ end
+
+ it 'returns 400 for invalid ID' do
+ post api('/users/999999/gpg_keys', admin)
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ describe 'GET /user/:id/gpg_keys' do
+ before do
+ admin
+ end
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api("/users/#{user.id}/gpg_keys")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns 404 for non-existing user' do
+ get api('/users/999999/gpg_keys', admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it 'returns 404 error if key not foud' do
+ delete api("/users/#{user.id}/gpg_keys/42", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+
+ it 'returns array of GPG keys' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ get api("/users/#{user.id}/gpg_keys", admin)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['key']).to eq(gpg_key.key)
+ end
+ end
+ end
+
+ describe 'DELETE /user/:id/gpg_keys/:key_id' do
+ before do
+ admin
+ end
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ delete api("/users/#{user.id}/keys/42")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'deletes existing key' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ expect do
+ delete api("/users/#{user.id}/gpg_keys/#{gpg_key.id}", admin)
+
+ expect(response).to have_http_status(204)
+ end.to change { user.gpg_keys.count }.by(-1)
+ end
+
+ it 'returns 404 error if user not found' do
+ user.keys << key
+ user.save
+
+ delete api("/users/999999/gpg_keys/#{gpg_key.id}", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it 'returns 404 error if key not foud' do
+ delete api("/users/#{user.id}/gpg_keys/42", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+ end
+ end
+
+ describe 'POST /user/:id/gpg_keys/:key_id/revoke' do
+ before do
+ admin
+ end
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ post api("/users/#{user.id}/gpg_keys/42/revoke")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'revokes existing key' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ expect do
+ post api("/users/#{user.id}/gpg_keys/#{gpg_key.id}/revoke", admin)
+
+ expect(response).to have_http_status(:accepted)
+ end.to change { user.gpg_keys.count }.by(-1)
+ end
+
+ it 'returns 404 error if user not found' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ post api("/users/999999/gpg_keys/#{gpg_key.id}/revoke", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it 'returns 404 error if key not foud' do
+ post api("/users/#{user.id}/gpg_keys/42/revoke", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+ end
+ end
+
describe "POST /users/:id/emails" do
before do
admin
@@ -1153,6 +1312,173 @@ describe API::Users do
end
end
+ describe 'GET /user/gpg_keys' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api('/user/gpg_keys')
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns array of GPG keys' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ get api('/user/gpg_keys', user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['key']).to eq(gpg_key.key)
+ end
+
+ context 'scopes' do
+ let(:path) { '/user/gpg_keys' }
+ let(:api_call) { method(:api) }
+
+ include_examples 'allows the "read_user" scope'
+ end
+ end
+ end
+
+ describe 'GET /user/gpg_keys/:key_id' do
+ it 'returns a single key' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ get api("/user/gpg_keys/#{gpg_key.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['key']).to eq(gpg_key.key)
+ end
+
+ it 'returns 404 Not Found within invalid ID' do
+ get api('/user/gpg_keys/42', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+
+ it "returns 404 error if admin accesses user's GPG key" do
+ user.gpg_keys << gpg_key
+ user.save
+
+ get api("/user/gpg_keys/#{gpg_key.id}", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+
+ it 'returns 404 for invalid ID' do
+ get api('/users/gpg_keys/ASDF', admin)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context 'scopes' do
+ let(:path) { "/user/gpg_keys/#{gpg_key.id}" }
+ let(:api_call) { method(:api) }
+
+ include_examples 'allows the "read_user" scope'
+ end
+ end
+
+ describe 'POST /user/gpg_keys' do
+ it 'creates a GPG key' do
+ key_attrs = attributes_for :gpg_key
+ expect do
+ post api('/user/gpg_keys', user), key_attrs
+
+ expect(response).to have_http_status(201)
+ end.to change { user.gpg_keys.count }.by(1)
+ end
+
+ it 'returns a 401 error if unauthorized' do
+ post api('/user/gpg_keys'), key: 'some key'
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'does not create GPG key without key' do
+ post api('/user/gpg_keys', user)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('key is missing')
+ end
+ end
+
+ describe 'POST /user/gpg_keys/:key_id/revoke' do
+ it 'revokes existing GPG key' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ expect do
+ post api("/user/gpg_keys/#{gpg_key.id}/revoke", user)
+
+ expect(response).to have_http_status(:accepted)
+ end.to change { user.gpg_keys.count}.by(-1)
+ end
+
+ it 'returns 404 if key ID not found' do
+ post api('/user/gpg_keys/42/revoke', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+
+ it 'returns 401 error if unauthorized' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ post api("/user/gpg_keys/#{gpg_key.id}/revoke")
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns a 404 for invalid ID' do
+ post api('/users/gpg_keys/ASDF/revoke', admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'DELETE /user/gpg_keys/:key_id' do
+ it 'deletes existing GPG key' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ expect do
+ delete api("/user/gpg_keys/#{gpg_key.id}", user)
+
+ expect(response).to have_http_status(204)
+ end.to change { user.gpg_keys.count}.by(-1)
+ end
+
+ it 'returns 404 if key ID not found' do
+ delete api('/user/gpg_keys/42', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+
+ it 'returns 401 error if unauthorized' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ delete api("/user/gpg_keys/#{gpg_key.id}")
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns a 404 for invalid ID' do
+ delete api('/users/gpg_keys/ASDF', admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
describe "GET /user/emails" do
context "when unauthenticated" do
it "returns authentication error" do