Commit 15ace6a9 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge commit '2be34630' into...

Merge commit '2be34630' into backstage/gb/rename-ci-cd-processing-sidekiq-queues

* commit '2be34630':
  Common Docker Documentation Location in `gitlab-ce`
  fix deprecation warning present during webpack compiles
  Enable 5 lines of Sidekiq backtrace lines to aid in debugging
  Add support for copying permalink to notes via more actions dropdown
  Handle creating a nested group on MySQL correctly
  Decrease statuses batch size even more in a migration
  Fix repo editor scrollbar
  Replace 'source/search_code.feature' spinach test with an rspec analog
  Authorizations regarding OAuth - style confirmation
  Update README.md
  Refactor complicated API group finding rules into GroupsFinder
  Fix group and project search for anonymous users
  Document version Group Milestones API introduced
  Allow v4 API GET requests for groups to be unauthenticated
  Adjust a range and a size in stages statuses migration
  Update README.md
  Point to /developers on docs/administration/authentiq.md
  Indexes GFM markdown guide
  use inline links instead of referenced
  Add index on ci_runners.contacted_at
parents e984a8a3 2be34630
...@@ -55,13 +55,18 @@ const Api = { ...@@ -55,13 +55,18 @@ const Api = {
// Return projects list. Filtered by query // Return projects list. Filtered by query
projects(query, options, callback) { projects(query, options, callback) {
const url = Api.buildUrl(Api.projectsPath); const url = Api.buildUrl(Api.projectsPath);
return $.ajax({ const defaults = {
url,
data: Object.assign({
search: query, search: query,
per_page: 20, per_page: 20,
membership: true, };
}, options),
if (gon.current_user_id) {
defaults.membership = true;
}
return $.ajax({
url,
data: Object.assign(defaults, options),
dataType: 'json', dataType: 'json',
}) })
.done(projects => callback(projects)); .done(projects => callback(projects));
......
...@@ -29,12 +29,14 @@ showTooltip = function(target, title) { ...@@ -29,12 +29,14 @@ showTooltip = function(target, title) {
var $target = $(target); var $target = $(target);
var originalTitle = $target.data('original-title'); var originalTitle = $target.data('original-title');
if (!$target.data('hideTooltip')) {
$target $target
.attr('title', 'Copied') .attr('title', 'Copied')
.tooltip('fixTitle') .tooltip('fixTitle')
.tooltip('show') .tooltip('show')
.attr('title', originalTitle) .attr('title', originalTitle)
.tooltip('fixTitle'); .tooltip('fixTitle');
}
}; };
$(function() { $(function() {
......
...@@ -16,6 +16,14 @@ body.modal-open { ...@@ -16,6 +16,14 @@ body.modal-open {
overflow: hidden; overflow: hidden;
} }
.modal-no-backdrop {
@extend .modal-dialog;
.modal-content {
box-shadow: none;
}
}
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
.modal-dialog { .modal-dialog {
width: 860px; width: 860px;
......
...@@ -182,7 +182,6 @@ ...@@ -182,7 +182,6 @@
padding: 5px 10px; padding: 5px 10px;
position: relative; position: relative;
border-top: 1px solid $white-normal; border-top: 1px solid $white-normal;
margin-top: -5px;
} }
#binary-viewer { #binary-viewer {
......
# GroupsFinder
#
# Used to filter Groups by a set of params
#
# Arguments:
# current_user - which user is requesting groups
# params:
# owned: boolean
# parent: Group
# all_available: boolean (defaults to true)
#
# Users with full private access can see all groups. The `owned` and `parent`
# params can be used to restrict the groups that are returned.
#
# Anonymous users will never return any `owned` groups. They will return all
# public groups instead, even if `all_available` is set to false.
class GroupsFinder < UnionFinder class GroupsFinder < UnionFinder
def initialize(current_user = nil, params = {}) def initialize(current_user = nil, params = {})
@current_user = current_user @current_user = current_user
...@@ -16,13 +32,13 @@ class GroupsFinder < UnionFinder ...@@ -16,13 +32,13 @@ class GroupsFinder < UnionFinder
attr_reader :current_user, :params attr_reader :current_user, :params
def all_groups def all_groups
groups = [] return [owned_groups] if params[:owned]
return [Group.all] if current_user&.full_private_access?
if current_user
groups << Gitlab::GroupHierarchy.new(groups_for_ancestors, groups_for_descendants).all_groups
end
groups << Group.unscoped.public_to_user(current_user)
groups = []
groups << Gitlab::GroupHierarchy.new(groups_for_ancestors, groups_for_descendants).all_groups if current_user
groups << Group.unscoped.public_to_user(current_user) if include_public_groups?
groups << Group.none if groups.empty?
groups groups
end end
...@@ -39,4 +55,12 @@ class GroupsFinder < UnionFinder ...@@ -39,4 +55,12 @@ class GroupsFinder < UnionFinder
groups.where(parent: params[:parent]) groups.where(parent: params[:parent])
end end
def owned_groups
current_user&.groups || Group.none
end
def include_public_groups?
current_user.nil? || params.fetch(:all_available, true)
end
end end
...@@ -20,6 +20,9 @@ module ButtonHelper ...@@ -20,6 +20,9 @@ module ButtonHelper
def clipboard_button(data = {}) def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard btn-transparent' css_class = data[:class] || 'btn-clipboard btn-transparent'
title = data[:title] || 'Copy to clipboard' title = data[:title] || 'Copy to clipboard'
button_text = data[:button_text] || ''
hide_tooltip = data[:hide_tooltip] || false
hide_button_icon = data[:hide_button_icon] || false
# This supports code in app/assets/javascripts/copy_to_clipboard.js that # This supports code in app/assets/javascripts/copy_to_clipboard.js that
# works around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM. # works around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM.
...@@ -35,17 +38,22 @@ module ButtonHelper ...@@ -35,17 +38,22 @@ module ButtonHelper
target = data.delete(:target) target = data.delete(:target)
data[:clipboard_target] = target if target data[:clipboard_target] = target if target
unless hide_tooltip
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
end
content_tag :button, button_attributes = {
icon('clipboard', 'aria-hidden': 'true'),
class: "btn #{css_class}", class: "btn #{css_class}",
data: data, data: data,
type: :button, type: :button,
title: title, title: title,
aria: { aria: { label: title }
label: title
} }
content_tag :button, button_attributes do
concat(icon('clipboard', 'aria-hidden': 'true')) unless hide_button_icon
concat(button_text)
end
end end
def http_clone_button(project, placement = 'right', append_link: true) def http_clone_button(project, placement = 'right', append_link: true)
......
...@@ -15,6 +15,10 @@ module Groups ...@@ -15,6 +15,10 @@ module Groups
return group return group
end end
if group_path.include?('/') && !Group.supports_nested_groups?
raise 'Nested groups are not supported on MySQL'
end
create_group_path create_group_path
end end
......
%h3.page-title Authorization required
%main{ :role => "main" } %main{ :role => "main" }
%p.h4 .modal-no-backdrop
.modal-content
.modal-header
%h3.page-title
Authorize Authorize
%strong.text-info= @pre_auth.client.name = link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer'
to use your account? to use your account?
.modal-body
- if current_user.admin? - if current_user.admin?
.text-warning.prepend-top-20 .text-warning
%p %p
= icon("exclamation-triangle fw") = icon("exclamation-triangle fw")
You are an admin, which means granting access to You are an admin, which means granting access to
%strong= @pre_auth.client.name %strong= @pre_auth.client.name
will allow them to interact with GitLab as an admin as well. Proceed with caution. will allow them to interact with GitLab as an admin as well. Proceed with caution.
%p
You are about to authorize
= link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer'
to use your account.
- if @pre_auth.scopes - if @pre_auth.scopes
#oauth-permissions This application will be able to:
%p This application will be able to: %ul
%ul.text-info
- @pre_auth.scopes.each do |scope| - @pre_auth.scopes.each do |scope|
%li= t scope, scope: [:doorkeeper, :scopes] %li= t scope, scope: [:doorkeeper, :scopes]
%hr/ .form-actions.text-right
.actions = form_tag oauth_authorization_path, method: :delete, class: 'inline' do
= form_tag oauth_authorization_path, method: :post do
= hidden_field_tag :client_id, @pre_auth.client.uid = hidden_field_tag :client_id, @pre_auth.client.uid
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
= hidden_field_tag :state, @pre_auth.state = hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope = hidden_field_tag :scope, @pre_auth.scope
= hidden_field_tag :nonce, @pre_auth.nonce = hidden_field_tag :nonce, @pre_auth.nonce
= submit_tag "Authorize", class: "btn btn-success wide pull-left" = submit_tag "Deny", class: "btn btn-danger"
= form_tag oauth_authorization_path, method: :delete do = form_tag oauth_authorization_path, method: :post, class: 'inline' do
= hidden_field_tag :client_id, @pre_auth.client.uid = hidden_field_tag :client_id, @pre_auth.client.uid
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
= hidden_field_tag :state, @pre_auth.state = hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope = hidden_field_tag :scope, @pre_auth.scope
= hidden_field_tag :nonce, @pre_auth.nonce = hidden_field_tag :nonce, @pre_auth.nonce
= submit_tag "Deny", class: "btn btn-danger prepend-left-10" = submit_tag "Authorize", class: "btn btn-success prepend-left-10"
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
%span.icon %span.icon
= custom_icon('ellipsis_v') = custom_icon('ellipsis_v')
%ul.dropdown-menu.more-actions-dropdown.dropdown-open-left %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left
%li
= clipboard_button(text: noteable_note_url(note), title: "Copy reference to clipboard", button_text: 'Copy link', hide_tooltip: true, hide_button_icon: true)
- unless is_current_user - unless is_current_user
%li %li
= link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
......
...@@ -11,5 +11,5 @@ ...@@ -11,5 +11,5 @@
%span.sr-only %span.sr-only
Clear search Clear search
- unless params[:snippets].eql? 'true' - unless params[:snippets].eql? 'true'
= render 'filter' if current_user = render 'filter'
= button_tag "Search", class: "btn btn-success btn-search" = button_tag "Search", class: "btn btn-success btn-search"
# Concern for enabling a few lines of exception backtraces in Sidekiq
module ExceptionBacktrace
extend ActiveSupport::Concern
included do
sidekiq_options backtrace: 5
end
end
class GroupDestroyWorker class GroupDestroyWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
include ExceptionBacktrace
def perform(group_id, user_id) def perform(group_id, user_id)
begin begin
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
class NamespacelessProjectDestroyWorker class NamespacelessProjectDestroyWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
include ExceptionBacktrace
def self.bulk_perform_async(args_list) def self.bulk_perform_async(args_list)
Sidekiq::Client.push_bulk('class' => self, 'queue' => sidekiq_options['queue'], 'args' => args_list) Sidekiq::Client.push_bulk('class' => self, 'queue' => sidekiq_options['queue'], 'args' => args_list)
......
class ProjectDestroyWorker class ProjectDestroyWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
include ExceptionBacktrace
def perform(project_id, user_id, params) def perform(project_id, user_id, params)
project = Project.find(project_id) project = Project.find(project_id)
......
class ProjectExportWorker class ProjectExportWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
include ExceptionBacktrace
sidekiq_options retry: 3 sidekiq_options retry: 3
......
...@@ -3,6 +3,7 @@ class RepositoryImportWorker ...@@ -3,6 +3,7 @@ class RepositoryImportWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
include ExceptionBacktrace
sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
......
---
title: Fix group and project search for anonymous users
merge_request: 13745
author:
type: fixed
---
title: restyling of OAuth authorization confirmation
merge_request:
author: Jacopo Beschi @jacopo-beschi
type: changed
---
title: Add support for copying permalink to notes via more actions dropdown
merge_request: 13299
author:
type: added
---
title: Document version Group Milestones API introduced
merge_request:
author:
type: changed
---
title: Replace 'source/search_code.feature' spinach test with an rspec analog
merge_request: 13697
author: blackst0ne
type: other
...@@ -170,7 +170,7 @@ var config = { ...@@ -170,7 +170,7 @@ var config = {
if (chunk.name) { if (chunk.name) {
return chunk.name; return chunk.name;
} }
return chunk.modules.map((m) => { return chunk.mapModules((m) => {
var chunkPath = m.request.split('!').pop(); var chunkPath = m.request.split('!').pop();
return path.relative(m.context, chunkPath); return path.relative(m.context, chunkPath);
}).join('_'); }).join('_');
......
...@@ -6,7 +6,7 @@ class MigrateStagesStatuses < ActiveRecord::Migration ...@@ -6,7 +6,7 @@ class MigrateStagesStatuses < ActiveRecord::Migration
disable_ddl_transaction! disable_ddl_transaction!
BATCH_SIZE = 10000 BATCH_SIZE = 10000
RANGE_SIZE = 1000 RANGE_SIZE = 100
MIGRATION = 'MigrateStageStatus'.freeze MIGRATION = 'MigrateStageStatus'.freeze
class Stage < ActiveRecord::Base class Stage < ActiveRecord::Base
...@@ -17,10 +17,10 @@ class MigrateStagesStatuses < ActiveRecord::Migration ...@@ -17,10 +17,10 @@ class MigrateStagesStatuses < ActiveRecord::Migration
def up def up
Stage.where(status: nil).each_batch(of: BATCH_SIZE) do |relation, index| Stage.where(status: nil).each_batch(of: BATCH_SIZE) do |relation, index|
relation.each_batch(of: RANGE_SIZE) do |batch| relation.each_batch(of: RANGE_SIZE) do |batch|
range = relation.pluck('MIN(id)', 'MAX(id)').first range = batch.pluck('MIN(id)', 'MAX(id)').first
schedule = index * 5.minutes delay = index * 5.minutes
BackgroundMigrationWorker.perform_in(schedule, MIGRATION, range) BackgroundMigrationWorker.perform_in(delay, MIGRATION, range)
end end
end end
end end
......
...@@ -4,7 +4,7 @@ To enable the Authentiq OmniAuth provider for passwordless authentication you mu ...@@ -4,7 +4,7 @@ To enable the Authentiq OmniAuth provider for passwordless authentication you mu
Authentiq will generate a Client ID and the accompanying Client Secret for you to use. Authentiq will generate a Client ID and the accompanying Client Secret for you to use.
1. Get your Client credentials (Client ID and Client Secret) at [Authentiq](https://www.authentiq.com/register). 1. Get your Client credentials (Client ID and Client Secret) at [Authentiq](https://www.authentiq.com/developers).
2. On your GitLab server, open the configuration file: 2. On your GitLab server, open the configuration file:
......
# Group milestones API # Group milestones API
> **Notes:**
> [Introduced][ce-12819] in GitLab 9.5.
## List group milestones ## List group milestones
Returns a list of group milestones. Returns a list of group milestones.
...@@ -118,3 +121,5 @@ Parameters: ...@@ -118,3 +121,5 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user - `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
- `milestone_id` (required) - The ID of a group milestone - `milestone_id` (required) - The ID of a group milestone
[ce-12819]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12819
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
## List groups ## List groups
Get a list of groups. (As user: my groups or all available, as admin: all groups). Get a list of visible groups for the authenticated user. When accessed without
authentication, only public groups are returned.
Parameters: Parameters:
...@@ -43,7 +44,8 @@ You can search for groups by name or path, see below. ...@@ -43,7 +44,8 @@ You can search for groups by name or path, see below.
## List a group's projects ## List a group's projects
Get a list of projects in this group. Get a list of projects in this group. When accessed without authentication, only
public projects are returned.
``` ```
GET /groups/:id/projects GET /groups/:id/projects
...@@ -109,7 +111,8 @@ Example response: ...@@ -109,7 +111,8 @@ Example response:
## Details of a group ## Details of a group
Get all details of a group. Get all details of a group. This endpoint can be accessed without authentication
if the group is publicly accessible.
``` ```
GET /groups/:id GET /groups/:id
......
...@@ -86,6 +86,11 @@ To follow conventions of naming across GitLab, and to futher move away from the ...@@ -86,6 +86,11 @@ To follow conventions of naming across GitLab, and to futher move away from the
`build` term and toward `job` CI variables have been renamed for the 9.0 `build` term and toward `job` CI variables have been renamed for the 9.0
release. release.
>**Note:**
Starting with GitLab 9.0, we have deprecated the `$CI_BUILD_*` variables. **You are
strongly advised to use the new variables as we will remove the old ones in
future GitLab releases.**
| 8.x name | 9.0+ name | | 8.x name | 9.0+ name |
| --------------------- |------------------------ | | --------------------- |------------------------ |
| `CI_BUILD_ID` | `CI_JOB_ID` | | `CI_BUILD_ID` | `CI_JOB_ID` |
......
...@@ -1065,6 +1065,8 @@ a list of all previous jobs from which the artifacts should be downloaded. ...@@ -1065,6 +1065,8 @@ a list of all previous jobs from which the artifacts should be downloaded.
You can only define jobs from stages that are executed before the current one. You can only define jobs from stages that are executed before the current one.
An error will be shown if you define jobs from the current stage or next ones. An error will be shown if you define jobs from the current stage or next ones.
Defining an empty array will skip downloading any artifacts for that job. Defining an empty array will skip downloading any artifacts for that job.
The status of the previous job is not considered when using `dependencies`, so
if it failed or it is a manual job that was not run, no error occurs.
--- ---
......
...@@ -113,13 +113,12 @@ merge request. ...@@ -113,13 +113,12 @@ merge request.
## Links ## Links
- If a link makes the paragraph to span across multiple lines, do not use - Use the regular inline link markdown markup `[Text](https://example.com)`.
the regular Markdown approach: `[Text](https://example.com)`. Instead use It's easier to read, review, and maintain.
`[Text][identifier]` and at the very bottom of the document add: - If there's a link that repeats several times through the same document,
`[identifier]: https://example.com`. This is another way to create Markdown you can use `[Text][identifier]` and at the bottom of the section or the
links which keeps the document clear and concise. Bonus points if you also document add: `[identifier]: https://example.com`, in which case, we do
add an alternative text: `[identifier]: https://example.com "Alternative text"` encourage you to also add an alternative text: `[identifier]: https://example.com "Alternative text"` that appears when hovering your mouse on a link.
that appears when hovering your mouse on a link
### Linking to inline docs ### Linking to inline docs
......
...@@ -17,7 +17,7 @@ the hardware requirements. ...@@ -17,7 +17,7 @@ the hardware requirements.
- [Installation from source](installation.md) - Install GitLab from source. - [Installation from source](installation.md) - Install GitLab from source.
Useful for unsupported systems like *BSD. For an overview of the directory Useful for unsupported systems like *BSD. For an overview of the directory
structure, read the [structure documentation](structure.md). structure, read the [structure documentation](structure.md).
- [Docker](https://docs.gitlab.com/omnibus/docker/) - Install GitLab using Docker. - [Docker](docker.md) - Install GitLab using Docker.
## Install GitLab on cloud providers ## Install GitLab on cloud providers
......
# GitLab Docker images
[Docker](https://www.docker.com) and container technology have been revolutionizing the software world for the past few years. They combine the performance and efficiency of native execution with the abstraction, security, and immutability of virtualization.
GitLab provides official Docker images to allowing you to easily take advantage of the benefits of containerization while operating your GitLab instance.
## Omnibus GitLab based images
GitLab maintains a set of [official Docker images](https://hub.docker.com/r/gitlab) based on our [Omnibus GitLab package](https://docs.gitlab.com/omnibus/README.html). These images include:
* [GitLab Community Edition](https://hub.docker.com/r/gitlab/gitlab-ce/)
* [GitLab Enterprise Edition](https://hub.docker.com/r/gitlab/gitlab-ee/)
* [GitLab Runner](https://hub.docker.com/r/gitlab/gitlab-runner/)
A [complete usage guide](https://docs.gitlab.com/omnibus/docker/) to these images is available, as well as the [Dockerfile used for building the images](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker).
## Cloud native images
GitLab is also working towards a [cloud native set of containers](https://gitlab.com/charts/helm.gitlab.io#docker-container-images), with a single image for each component service. We intend for these images to eventually replace the [Omnibus GitLab based images](#omnibus-gitlab-based-images).
...@@ -119,6 +119,13 @@ When performing inline reviews to implementations ...@@ -119,6 +119,13 @@ When performing inline reviews to implementations
to your codebase through merge requests you can to your codebase through merge requests you can
gather feedback through [resolvable discussions](discussions/index.md#resolvable-discussions). gather feedback through [resolvable discussions](discussions/index.md#resolvable-discussions).
### GitLab Flavored Markdown (GFM)
Read through the [GFM documentation](markdown.md) to learn how to apply
the best of GitLab Flavored Markdown in your discussions, comments,
issues and merge requests descriptions, and everywhere else GMF is
supported.
## Todos ## Todos
Never forget to reply to your collaborators. [GitLab Todos](../workflow/todos.md) Never forget to reply to your collaborators. [GitLab Todos](../workflow/todos.md)
......
# GitLab Docker images # GitLab Docker images
* The official GitLab Community Edition Docker image is [available on Docker Hub](https://hub.docker.com/r/gitlab/gitlab-ce/). This content has been moved to [our documentation site](https://docs.gitlab.com/ce/install/docker.html).
* The official GitLab Enterprise Edition Docker image is [available on Docker Hub](https://hub.docker.com/r/gitlab/gitlab-ee/).
* The complete usage guide can be found in [Using GitLab Docker images](https://docs.gitlab.com/omnibus/docker/)
* The Dockerfile used for building public images is in [Omnibus Repository](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker)
* Check the guide for [creating Omnibus-based Docker Image](https://docs.gitlab.com/omnibus/build/README.html#build-docker-image)
Feature: Project Source Search Code
Background:
Given I sign in as a user
Scenario: Search for term "coffee"
Given I own project "Shop"
And I visit project source page
When I search for term "coffee"
Then I should see files from repository containing "coffee"
Scenario: Search on empty project
Given I own an empty project
And I visit my project's home page
When I search for term "coffee"
Then I should see empty result
class Spinach::Features::ProjectSourceSearchCode < Spinach::FeatureSteps
include SharedAuthentication
include SharedProject
include SharedPaths
step 'I search for term "coffee"' do
fill_in "search", with: "coffee"
click_button "Go"
end
step 'I should see files from repository containing "coffee"' do
expect(page).to have_content 'coffee'
expect(page).to have_content 'CONTRIBUTING.md'
end
step 'I should see empty result' do
expect(page).to have_content "We couldn't find any"
end
end
...@@ -304,10 +304,6 @@ module SharedPaths ...@@ -304,10 +304,6 @@ module SharedPaths
visit project_commits_path(@project, 'stable', { limit: 5 }) visit project_commits_path(@project, 'stable', { limit: 5 })
end end
step 'I visit project source page' do
visit project_tree_path(@project, root_ref)
end
step 'I visit blob file from repo' do step 'I visit blob file from repo' do
visit project_blob_path(@project, File.join(sample_commit.id, sample_blob.path)) visit project_blob_path(@project, File.join(sample_commit.id, sample_blob.path))
end end
......
...@@ -2,7 +2,7 @@ module API ...@@ -2,7 +2,7 @@ module API
class Groups < Grape::API class Groups < Grape::API
include PaginationParams include PaginationParams
before { authenticate! } before { authenticate_non_get! }
helpers do helpers do
params :optional_params_ce do params :optional_params_ce do
...@@ -47,16 +47,8 @@ module API ...@@ -47,16 +47,8 @@ module API
use :pagination use :pagination
end end
get do get do
groups = if params[:owned] find_params = { all_available: params[:all_available], owned: params[:owned] }
current_user.owned_groups groups = GroupsFinder.new(current_user, find_params).execute
elsif current_user.admin
Group.all
elsif params[:all_available]
GroupsFinder.new(current_user).execute
else
current_user.groups
end
groups = groups.search(params[:search]) if params[:search].present? groups = groups.search(params[:search]) if params[:search].present?
groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present? groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
groups = groups.reorder(params[:order_by] => params[:sort]) groups = groups.reorder(params[:order_by] => params[:sort])
......
require 'spec_helper'
feature 'Find files button in the tree header' do
given(:user) { create(:user) }
given(:project) { create(:project, :repository) }
background do
sign_in(user)
project.team << [user, :developer]
end
scenario 'project main screen' do
visit project_path(project)
expect(page).to have_selector('.tree-controls .shortcuts-find-file')
end
scenario 'project tree screen' do
visit project_tree_path(project, project.default_branch)
expect(page).to have_selector('.tree-controls .shortcuts-find-file')
end
end
require 'spec_helper'
describe 'User searches for files' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
before do
sign_in(user)
end
describe 'project main screen' do
context 'when project is empty' do
let(:empty_project) { create(:project) }
before do
empty_project.add_developer(user)
visit project_path(empty_project)
end
it 'does not show any result' do
fill_in('search', with: 'coffee')
click_button('Go')
expect(page).to have_content("We couldn't find any")
end
end
context 'when project is not empty' do
before do
project.add_developer(user)
visit project_path(project)
end
it 'shows "Find file" button' do
expect(page).to have_selector('.tree-controls .shortcuts-find-file')
end
end
end
describe 'project tree screen' do
before do
project.add_developer(user)
visit project_tree_path(project, project.default_branch)
end
it 'shows "Find file" button' do
expect(page).to have_selector('.tree-controls .shortcuts-find-file')
end
it 'shows found files' do
fill_in('search', with: 'coffee')
click_button('Go')
expect(page).to have_content('coffee')
expect(page).to have_content('CONTRIBUTING.md')
end
end
end
...@@ -281,4 +281,30 @@ describe "Search" do ...@@ -281,4 +281,30 @@ describe "Search" do
expect(page).to have_selector('.commit-row-description', count: 9) expect(page).to have_selector('.commit-row-description', count: 9)
end end
end end
context 'anonymous user' do
let(:project) { create(:project, :public) }
before do
sign_out(user)
end
it 'preserves the group being searched in' do
visit search_path(group_id: project.namespace.id)
fill_in 'search', with: 'foo'
click_button 'Search'
expect(find('#group_id').value).to eq(project.namespace.id.to_s)
end
it 'preserves the project being searched in' do
visit search_path(project_id: project.id)
fill_in 'search', with: 'foo'
click_button 'Search'
expect(find('#project_id').value).to eq(project.id.to_s)
end
end
end end
...@@ -62,4 +62,67 @@ describe ButtonHelper do ...@@ -62,4 +62,67 @@ describe ButtonHelper do
end end
end end
end end
describe 'clipboard_button' do
let(:user) { create(:user) }
let(:project) { build_stubbed(:project) }
def element(data = {})
element = helper.clipboard_button(data)
Nokogiri::HTML::DocumentFragment.parse(element).first_element_child
end
before do
allow(helper).to receive(:current_user).and_return(user)
end
context 'with default options' do
context 'when no `text` attribute is not provided' do
it 'shows copy to clipboard button with default configuration and no text set to copy' do
expect(element.attr('class')).to eq('btn btn-clipboard btn-transparent')
expect(element.attr('type')).to eq('button')
expect(element.attr('aria-label')).to eq('Copy to clipboard')
expect(element.attr('data-toggle')).to eq('tooltip')
expect(element.attr('data-placement')).to eq('bottom')
expect(element.attr('data-container')).to eq('body')
expect(element.attr('data-clipboard-text')).to eq(nil)
expect(element.inner_text).to eq("")
expect(element).to have_selector('.fa.fa-clipboard')
end
end
context 'when `text` attribute is provided' do
it 'shows copy to clipboard button with provided `text` to copy' do
expect(element(text: 'Hello World!').attr('data-clipboard-text')).to eq('Hello World!')
end
end
context 'when `title` attribute is provided' do
it 'shows copy to clipboard button with provided `title` as tooltip' do
expect(element(title: 'Copy to my clipboard!').attr('aria-label')).to eq('Copy to my clipboard!')
end
end
end
context 'with `button_text` attribute provided' do
it 'shows copy to clipboard button with provided `button_text` as button label' do
expect(element(button_text: 'Copy text').inner_text).to eq('Copy text')
end
end
context 'with `hide_tooltip` attribute provided' do
it 'shows copy to clipboard button without tooltip support' do
expect(element(hide_tooltip: true).attr('data-placement')).to eq(nil)
expect(element(hide_tooltip: true).attr('data-toggle')).to eq(nil)
expect(element(hide_tooltip: true).attr('data-container')).to eq(nil)
end
end
context 'with `hide_button_icon` attribute provided' do
it 'shows copy to clipboard button without tooltip support' do
expect(element(hide_button_icon: true)).not_to have_selector('.fa.fa-clipboard')
end
end
end
end end
...@@ -17,7 +17,7 @@ describe('Api', () => { ...@@ -17,7 +17,7 @@ describe('Api', () => {
beforeEach(() => { beforeEach(() => {
originalGon = window.gon; originalGon = window.gon;
window.gon = dummyGon; window.gon = Object.assign({}, dummyGon);
}); });
afterEach(() => { afterEach(() => {
...@@ -98,10 +98,11 @@ describe('Api', () => { ...@@ -98,10 +98,11 @@ describe('Api', () => {
}); });
describe('projects', () => { describe('projects', () => {
it('fetches projects', (done) => { it('fetches projects with membership when logged in', (done) => {
const query = 'dummy query'; const query = 'dummy query';
const options = { unused: 'option' }; const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`;
window.gon.current_user_id = 1;
const expectedData = Object.assign({ const expectedData = Object.assign({
search: query, search: query,
per_page: 20, per_page: 20,
...@@ -119,6 +120,27 @@ describe('Api', () => { ...@@ -119,6 +120,27 @@ describe('Api', () => {
done(); done();
}); });
}); });
it('fetches projects without membership when not logged in', (done) => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`;
const expectedData = Object.assign({
search: query,
per_page: 20,
}, options);
spyOn(jQuery, 'ajax').and.callFake((request) => {
expect(request.url).toEqual(expectedUrl);
expect(request.dataType).toEqual('json');
expect(request.data).toEqual(expectedData);
return sendDummyResponse();
});
Api.projects(query, options, (response) => {
expect(response).toBe(dummyResponse);
done();
});
});
}); });
describe('newLabel', () => { describe('newLabel', () => {
......
...@@ -7,6 +7,7 @@ import '~/project_select'; ...@@ -7,6 +7,7 @@ import '~/project_select';
import '~/project'; import '~/project';
describe('Project Title', () => { describe('Project Title', () => {
const dummyApiVersion = 'v3000';
preloadFixtures('issues/open-issue.html.raw'); preloadFixtures('issues/open-issue.html.raw');
loadJSONFixtures('projects.json'); loadJSONFixtures('projects.json');
...@@ -14,7 +15,7 @@ describe('Project Title', () => { ...@@ -14,7 +15,7 @@ describe('Project Title', () => {
loadFixtures('issues/open-issue.html.raw'); loadFixtures('issues/open-issue.html.raw');
window.gon = {}; window.gon = {};
window.gon.api_version = 'v3'; window.gon.api_version = dummyApiVersion;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Project(); new Project();
...@@ -37,9 +38,10 @@ describe('Project Title', () => { ...@@ -37,9 +38,10 @@ describe('Project Title', () => {
it('toggles dropdown', () => { it('toggles dropdown', () => {
const $menu = $('.js-dropdown-menu-projects'); const $menu = $('.js-dropdown-menu-projects');
window.gon.current_user_id = 1;
$('.js-projects-dropdown-toggle').click(); $('.js-projects-dropdown-toggle').click();
expect($menu).toHaveClass('open'); expect($menu).toHaveClass('open');
expect(reqUrl).toBe('/api/v3/projects.json?simple=true'); expect(reqUrl).toBe(`/api/${dummyApiVersion}/projects.json?simple=true`);
expect(reqData).toEqual({ expect(reqData).toEqual({
search: '', search: '',
order_by: 'last_activity_at', order_by: 'last_activity_at',
......
...@@ -2,13 +2,14 @@ require 'spec_helper' ...@@ -2,13 +2,14 @@ require 'spec_helper'
describe Gitlab::BareRepositoryImporter, repository: true do describe Gitlab::BareRepositoryImporter, repository: true do
subject(:importer) { described_class.new('default', project_path) } subject(:importer) { described_class.new('default', project_path) }
let(:project_path) { 'a-group/a-sub-group/a-project' }
let!(:admin) { create(:admin) } let!(:admin) { create(:admin) }
before do before do
allow(described_class).to receive(:log) allow(described_class).to receive(:log)
end end
shared_examples 'importing a repository' do
describe '.execute' do describe '.execute' do
it 'creates a project for a repository in storage' do it 'creates a project for a repository in storage' do
FileUtils.mkdir_p(File.join(TestEnv.repos_path, "#{project_path}.git")) FileUtils.mkdir_p(File.join(TestEnv.repos_path, "#{project_path}.git"))
...@@ -49,12 +50,10 @@ describe Gitlab::BareRepositoryImporter, repository: true do ...@@ -49,12 +50,10 @@ describe Gitlab::BareRepositoryImporter, repository: true do
end end
it 'skips importing when the project already exists' do it 'skips importing when the project already exists' do
group = create(:group, path: 'a-group') project = create(:project, path: 'a-project', namespace: existing_group)
subgroup = create(:group, path: 'a-sub-group', parent: group)
project = create(:project, path: 'a-project', namespace: subgroup)
expect(importer).not_to receive(:create_project) expect(importer).not_to receive(:create_project)
expect(importer).to receive(:log).with(" * #{project.name} (a-group/a-sub-group/a-project) exists") expect(importer).to receive(:log).with(" * #{project.name} (#{project_path}) exists")
importer.create_project_if_needed importer.create_project_if_needed
end end
...@@ -65,4 +64,37 @@ describe Gitlab::BareRepositoryImporter, repository: true do ...@@ -65,4 +64,37 @@ describe Gitlab::BareRepositoryImporter, repository: true do
expect(Project.find_by_full_path(project_path)).not_to be_nil expect(Project.find_by_full_path(project_path)).not_to be_nil
end end
end end
end
context 'with subgroups', :nested_groups do
let(:project_path) { 'a-group/a-sub-group/a-project' }
let(:existing_group) do
group = create(:group, path: 'a-group')
create(:group, path: 'a-sub-group', parent: group)
end
it_behaves_like 'importing a repository'
end
context 'without subgroups' do
let(:project_path) { 'a-group/a-project' }
let(:existing_group) { create(:group, path: 'a-group') }
it_behaves_like 'importing a repository'
end
context 'when subgroups are not available' do
let(:project_path) { 'a-group/a-sub-group/a-project' }
before do
expect(Group).to receive(:supports_nested_groups?) { false }
end
describe '#create_project_if_needed' do
it 'raises an error' do
expect { importer.create_project_if_needed }.to raise_error('Nested groups are not supported on MySQL')
end
end
end
end end
...@@ -12,7 +12,7 @@ describe MigrateStagesStatuses, :migration do ...@@ -12,7 +12,7 @@ describe MigrateStagesStatuses, :migration do
before do before do
stub_const("#{described_class.name}::BATCH_SIZE", 2) stub_const("#{described_class.name}::BATCH_SIZE", 2)
stub_const("#{described_class.name}::RANGE_SIZE", 2) stub_const("#{described_class.name}::RANGE_SIZE", 1)
projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1') projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1')
projects.create!(id: 2, name: 'gitlab2', path: 'gitlab2') projects.create!(id: 2, name: 'gitlab2', path: 'gitlab2')
...@@ -50,9 +50,10 @@ describe MigrateStagesStatuses, :migration do ...@@ -50,9 +50,10 @@ describe MigrateStagesStatuses, :migration do
Timecop.freeze do Timecop.freeze do
migrate! migrate!
expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 1, 2) expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 1, 1)
expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 2, 2)
expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 3, 3) expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 3, 3)
expect(BackgroundMigrationWorker.jobs.size).to eq 2 expect(BackgroundMigrationWorker.jobs.size).to eq 3
end end
end end
end end
......
...@@ -20,10 +20,15 @@ describe API::Groups do ...@@ -20,10 +20,15 @@ describe API::Groups do
describe "GET /groups" do describe "GET /groups" do
context "when unauthenticated" do context "when unauthenticated" do
it "returns authentication error" do it "returns public groups" do
get api("/groups") get api("/groups")
expect(response).to have_http_status(401) expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response)
.to satisfy_one { |group| group['name'] == group1.name }
end end
end end
...@@ -165,6 +170,18 @@ describe API::Groups do ...@@ -165,6 +170,18 @@ describe API::Groups do
end end
describe "GET /groups/:id" do describe "GET /groups/:id" do
context 'when unauthenticated' do
it 'returns 404 for a private group' do
get api("/groups/#{group2.id}")
expect(response).to have_http_status(404)
end
it 'returns 200 for a public group' do
get api("/groups/#{group1.id}")
expect(response).to have_http_status(200)
end
end
context "when authenticated as user" do context "when authenticated as user" do
it "returns one of user1's groups" do it "returns one of user1's groups" do
project = create(:project, namespace: group2, path: 'Foo') project = create(:project, namespace: group2, path: 'Foo')
......
...@@ -2,10 +2,61 @@ require 'spec_helper' ...@@ -2,10 +2,61 @@ require 'spec_helper'
describe Groups::NestedCreateService do describe Groups::NestedCreateService do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:params) { { group_path: 'a-group/a-sub-group' } }
subject(:service) { described_class.new(user, params) } subject(:service) { described_class.new(user, params) }
shared_examples 'with a visibility level' do
it 'creates the group with correct visibility level' do
allow(Gitlab::CurrentSettings.current_application_settings)
.to receive(:default_group_visibility) { Gitlab::VisibilityLevel::INTERNAL }
group = service.execute
expect(group.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
context 'adding a visibility level ' do
it 'overwrites the visibility level' do
service = described_class.new(user, params.merge(visibility_level: Gitlab::VisibilityLevel::PRIVATE))
group = service.execute
expect(group.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
end
end
describe 'without subgroups' do
let(:params) { { group_path: 'a-group' } }
before do
allow(Group).to receive(:supports_nested_groups?) { false }
end
it 'creates the group' do
group = service.execute
expect(group).to be_persisted
end
it 'returns the group if it already existed' do
existing_group = create(:group, path: 'a-group')
expect(service.execute).to eq(existing_group)
end
it 'raises an error when tring to create a subgroup' do
service = described_class.new(user, group_path: 'a-group/a-sub-group')
expect { service.execute }.to raise_error('Nested groups are not supported on MySQL')
end
it_behaves_like 'with a visibility level'
end
describe 'with subgroups', :nested_groups do
let(:params) { { group_path: 'a-group/a-sub-group' } }
describe "#execute" do describe "#execute" do
it 'returns the group if it already existed' do it 'returns the group if it already existed' do
parent = create(:group, path: 'a-group', owner: user) parent = create(:group, path: 'a-group', owner: user)
...@@ -14,14 +65,14 @@ describe Groups::NestedCreateService do ...@@ -14,14 +65,14 @@ describe Groups::NestedCreateService do
expect(service.execute).to eq(child) expect(service.execute).to eq(child)
end end
it 'reuses a parent if it already existed', :nested_groups do it 'reuses a parent if it already existed' do
parent = create(:group, path: 'a-group') parent = create(:group, path: 'a-group')
parent.add_owner(user) parent.add_owner(user)
expect(service.execute.parent).to eq(parent) expect(service.execute.parent).to eq(parent)
end end
it 'creates group and subgroup in the database', :nested_groups do it 'creates group and subgroup in the database' do
service.execute service.execute
parent = Group.find_by_full_path('a-group') parent = Group.find_by_full_path('a-group')
...@@ -31,23 +82,7 @@ describe Groups::NestedCreateService do ...@@ -31,23 +82,7 @@ describe Groups::NestedCreateService do
expect(child).not_to be_nil expect(child).not_to be_nil
end end
it 'creates the group with correct visibility level' do it_behaves_like 'with a visibility level'
allow(Gitlab::CurrentSettings.current_application_settings)
.to receive(:default_group_visibility) { Gitlab::VisibilityLevel::INTERNAL }
group = service.execute
expect(group.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
context 'adding a visibility level ' do
let(:params) { { group_path: 'a-group/a-sub-group', visibility_level: Gitlab::VisibilityLevel::PRIVATE } }
it 'overwrites the visibility level' do
group = service.execute
expect(group.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment