diff options
36 files changed, 419 insertions, 144 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 4319bfcc57f..78cb3def879 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -55,13 +55,18 @@ const Api = { // Return projects list. Filtered by query projects(query, options, callback) { const url = Api.buildUrl(Api.projectsPath); + const defaults = { + search: query, + per_page: 20, + }; + + if (gon.current_user_id) { + defaults.membership = true; + } + return $.ajax({ url, - data: Object.assign({ - search: query, - per_page: 20, - membership: true, - }, options), + data: Object.assign(defaults, options), dataType: 'json', }) .done(projects => callback(projects)); diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js index ab9a8e43dd1..1f3c7e1772d 100644 --- a/app/assets/javascripts/copy_to_clipboard.js +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -29,12 +29,14 @@ showTooltip = function(target, title) { var $target = $(target); var originalTitle = $target.data('original-title'); - $target - .attr('title', 'Copied') - .tooltip('fixTitle') - .tooltip('show') - .attr('title', originalTitle) - .tooltip('fixTitle'); + if (!$target.data('hideTooltip')) { + $target + .attr('title', 'Copied') + .tooltip('fixTitle') + .tooltip('show') + .attr('title', originalTitle) + .tooltip('fixTitle'); + } }; $(function() { diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index d1f00d3ee2c..5b581780447 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -16,6 +16,14 @@ body.modal-open { overflow: hidden; } +.modal-no-backdrop { + @extend .modal-dialog; + + .modal-content { + box-shadow: none; + } +} + @media (min-width: $screen-md-min) { .modal-dialog { width: 860px; diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index e6fb112e7f2..88d71b0a87b 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -1,3 +1,19 @@ +# 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 def initialize(current_user = nil, params = {}) @current_user = current_user @@ -16,13 +32,13 @@ class GroupsFinder < UnionFinder attr_reader :current_user, :params def all_groups - groups = [] - - if current_user - groups << Gitlab::GroupHierarchy.new(groups_for_ancestors, groups_for_descendants).all_groups - end - groups << Group.unscoped.public_to_user(current_user) + return [owned_groups] if params[:owned] + return [Group.all] if current_user&.full_private_access? + 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 end @@ -39,4 +55,12 @@ class GroupsFinder < UnionFinder groups.where(parent: params[:parent]) end + + def owned_groups + current_user&.groups || Group.none + end + + def include_public_groups? + current_user.nil? || params.fetch(:all_available, true) + end end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index bf9ad95b7c2..48cf30a48ab 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -20,6 +20,9 @@ module ButtonHelper def clipboard_button(data = {}) css_class = data[:class] || 'btn-clipboard btn-transparent' 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 # works around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM. @@ -35,17 +38,22 @@ module ButtonHelper target = data.delete(:target) data[:clipboard_target] = target if target - data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) + unless hide_tooltip + data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) + end - content_tag :button, - icon('clipboard', 'aria-hidden': 'true'), + button_attributes = { class: "btn #{css_class}", data: data, type: :button, title: title, - aria: { - label: title - } + aria: { label: title } + } + + content_tag :button, button_attributes do + concat(icon('clipboard', 'aria-hidden': 'true')) unless hide_button_icon + concat(button_text) + end end def http_clone_button(project, placement = 'right', append_link: true) diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 4ee972fa68d..754c37518b3 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -17,6 +17,10 @@ module Ci validates :pipeline, presence: true, unless: :importing? validates :name, presence: true, unless: :importing? + after_initialize do |stage| + self.status = DEFAULT_STATUS if self.status.nil? + end + state_machine :status, initial: :created do event :enqueue do transition created: :pending diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index 82aa51f9778..8ba88906714 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -1,39 +1,43 @@ -%h3.page-title Authorization required %main{ :role => "main" } - %p.h4 - Authorize - %strong.text-info= @pre_auth.client.name - to use your account? + .modal-no-backdrop + .modal-content + .modal-header + %h3.page-title + Authorize + = link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer' + to use your account? - - if current_user.admin? - .text-warning.prepend-top-20 - %p - = icon("exclamation-triangle fw") - You are an admin, which means granting access to - %strong= @pre_auth.client.name - will allow them to interact with GitLab as an admin as well. Proceed with caution. - - - if @pre_auth.scopes - #oauth-permissions - %p This application will be able to: - %ul.text-info - - @pre_auth.scopes.each do |scope| - %li= t scope, scope: [:doorkeeper, :scopes] - %hr/ - .actions - = form_tag oauth_authorization_path, method: :post do - = hidden_field_tag :client_id, @pre_auth.client.uid - = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri - = hidden_field_tag :state, @pre_auth.state - = hidden_field_tag :response_type, @pre_auth.response_type - = hidden_field_tag :scope, @pre_auth.scope - = hidden_field_tag :nonce, @pre_auth.nonce - = submit_tag "Authorize", class: "btn btn-success wide pull-left" - = form_tag oauth_authorization_path, method: :delete do - = hidden_field_tag :client_id, @pre_auth.client.uid - = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri - = hidden_field_tag :state, @pre_auth.state - = hidden_field_tag :response_type, @pre_auth.response_type - = hidden_field_tag :scope, @pre_auth.scope - = hidden_field_tag :nonce, @pre_auth.nonce - = submit_tag "Deny", class: "btn btn-danger prepend-left-10" + .modal-body + - if current_user.admin? + .text-warning + %p + = icon("exclamation-triangle fw") + You are an admin, which means granting access to + %strong= @pre_auth.client.name + 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 + This application will be able to: + %ul + - @pre_auth.scopes.each do |scope| + %li= t scope, scope: [:doorkeeper, :scopes] + .form-actions.text-right + = form_tag oauth_authorization_path, method: :delete, class: 'inline' do + = hidden_field_tag :client_id, @pre_auth.client.uid + = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri + = hidden_field_tag :state, @pre_auth.state + = hidden_field_tag :response_type, @pre_auth.response_type + = hidden_field_tag :scope, @pre_auth.scope + = hidden_field_tag :nonce, @pre_auth.nonce + = submit_tag "Deny", class: "btn btn-danger" + = form_tag oauth_authorization_path, method: :post, class: 'inline' do + = hidden_field_tag :client_id, @pre_auth.client.uid + = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri + = hidden_field_tag :state, @pre_auth.state + = hidden_field_tag :response_type, @pre_auth.response_type + = hidden_field_tag :scope, @pre_auth.scope + = hidden_field_tag :nonce, @pre_auth.nonce + = submit_tag "Authorize", class: "btn btn-success prepend-left-10" diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index ad5befc6ee5..de0f1de057d 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -32,7 +32,8 @@ %ul - if can_update_issue %li= link_to 'Edit', edit_project_issue_path(@project, @issue) - - unless current_user == @issue.author + / TODO: simplify condition back #36860 + - if @issue.author && current_user != @issue.author %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue)) - if can_update_issue %li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml index 5930209a682..7e854186973 100644 --- a/app/views/projects/notes/_more_actions_dropdown.html.haml +++ b/app/views/projects/notes/_more_actions_dropdown.html.haml @@ -6,6 +6,8 @@ %span.icon = custom_icon('ellipsis_v') %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 %li = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml index 3139be1cd37..a4a5cec1314 100644 --- a/app/views/search/_form.html.haml +++ b/app/views/search/_form.html.haml @@ -11,5 +11,5 @@ %span.sr-only Clear search - unless params[:snippets].eql? 'true' - = render 'filter' if current_user + = render 'filter' = button_tag "Search", class: "btn btn-success btn-search" diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml index 8a1268a1c6d..f22b6c9a6c2 100644 --- a/app/views/shared/issuable/_close_reopen_button.html.haml +++ b/app/views/shared/issuable/_close_reopen_button.html.haml @@ -9,6 +9,7 @@ class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" - elsif can_update && !is_current_user = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable -- else +- elsif issuable.author + / TODO: change back to else #36860 = link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), class: 'hidden-xs hidden-sm btn btn-grouped btn-close-color', title: 'Report abuse' diff --git a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml index 6756a7f17fd..daa05990ae9 100644 --- a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml +++ b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml @@ -37,13 +37,15 @@ %li.divider.droplab-item-ignore - %li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), - button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } } - %button.btn.btn-transparent - = icon('check', class: 'icon') - .description - %strong.title Report abuse - %p.text - Report - = display_issuable_type.pluralize - that are abusive, inappropriate or spam. + / TODO: remove condition #36860 + - if issuable.author + %li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), + button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } } + %button.btn.btn-transparent + = icon('check', class: 'icon') + .description + %strong.title Report abuse + %p.text + Report + = display_issuable_type.pluralize + that are abusive, inappropriate or spam. diff --git a/changelogs/unreleased/31409-fix-group-and-project-search-for-anonymous-users.yml b/changelogs/unreleased/31409-fix-group-and-project-search-for-anonymous-users.yml new file mode 100644 index 00000000000..06e8180db64 --- /dev/null +++ b/changelogs/unreleased/31409-fix-group-and-project-search-for-anonymous-users.yml @@ -0,0 +1,5 @@ +--- +title: Fix group and project search for anonymous users +merge_request: 13745 +author: +type: fixed diff --git a/changelogs/unreleased/35721-auth-style-confirmation.yml b/changelogs/unreleased/35721-auth-style-confirmation.yml new file mode 100644 index 00000000000..9963f76e845 --- /dev/null +++ b/changelogs/unreleased/35721-auth-style-confirmation.yml @@ -0,0 +1,5 @@ +--- +title: restyling of OAuth authorization confirmation +merge_request: +author: Jacopo Beschi @jacopo-beschi +type: changed diff --git a/changelogs/unreleased/35811-copy-link-note.yml b/changelogs/unreleased/35811-copy-link-note.yml new file mode 100644 index 00000000000..9fa74884c8a --- /dev/null +++ b/changelogs/unreleased/35811-copy-link-note.yml @@ -0,0 +1,5 @@ +--- +title: Add support for copying permalink to notes via more actions dropdown +merge_request: 13299 +author: +type: added diff --git a/changelogs/unreleased/36860-deleted-user-fix.yml b/changelogs/unreleased/36860-deleted-user-fix.yml new file mode 100644 index 00000000000..79e75441134 --- /dev/null +++ b/changelogs/unreleased/36860-deleted-user-fix.yml @@ -0,0 +1,5 @@ +--- +title: Fix failure when issue is authored by a deleted user +merge_request: 13807 +author: +type: fixed diff --git a/changelogs/unreleased/36939-fix-find-blobs-by-path.yml b/changelogs/unreleased/36939-fix-find-blobs-by-path.yml new file mode 100644 index 00000000000..b48b10049ed --- /dev/null +++ b/changelogs/unreleased/36939-fix-find-blobs-by-path.yml @@ -0,0 +1,5 @@ +--- +title: Fix searching for files by path +merge_request: 13798 +author: +type: fixed diff --git a/changelogs/unreleased/revert-appearances-description-html-not-null.yml b/changelogs/unreleased/revert-appearances-description-html-not-null.yml new file mode 100644 index 00000000000..4e3c39cb5fd --- /dev/null +++ b/changelogs/unreleased/revert-appearances-description-html-not-null.yml @@ -0,0 +1,5 @@ +--- +title: Re-allow appearances.description_html to be NULL +merge_request: +author: +type: fixed diff --git a/db/migrate/20170809142252_cleanup_appearances_schema.rb b/db/migrate/20170809142252_cleanup_appearances_schema.rb index 90d12925ba2..acf45060114 100644 --- a/db/migrate/20170809142252_cleanup_appearances_schema.rb +++ b/db/migrate/20170809142252_cleanup_appearances_schema.rb @@ -7,7 +7,7 @@ class CleanupAppearancesSchema < ActiveRecord::Migration # Set this constant to true if this migration requires downtime. DOWNTIME = false - NOT_NULL_COLUMNS = %i[title description description_html created_at updated_at] + NOT_NULL_COLUMNS = %i[title description created_at updated_at] TIME_COLUMNS = %i[created_at updated_at] diff --git a/db/migrate/20170824162758_allow_appearances_description_html_null.rb b/db/migrate/20170824162758_allow_appearances_description_html_null.rb new file mode 100644 index 00000000000..d7f481ee894 --- /dev/null +++ b/db/migrate/20170824162758_allow_appearances_description_html_null.rb @@ -0,0 +1,18 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AllowAppearancesDescriptionHtmlNull < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def up + change_column_null :appearances, :description_html, true + end + + def down + # This column should not have a `NOT NULL` class, so we don't want to revert + # back to re-adding it. + end +end diff --git a/db/post_migrate/20170711145558_migrate_stages_statuses.rb b/db/post_migrate/20170711145558_migrate_stages_statuses.rb index 5a24fb1307f..aeb900354db 100644 --- a/db/post_migrate/20170711145558_migrate_stages_statuses.rb +++ b/db/post_migrate/20170711145558_migrate_stages_statuses.rb @@ -6,7 +6,7 @@ class MigrateStagesStatuses < ActiveRecord::Migration disable_ddl_transaction! BATCH_SIZE = 10000 - RANGE_SIZE = 1000 + RANGE_SIZE = 100 MIGRATION = 'MigrateStageStatus'.freeze class Stage < ActiveRecord::Base @@ -17,10 +17,10 @@ class MigrateStagesStatuses < ActiveRecord::Migration def up Stage.where(status: nil).each_batch(of: BATCH_SIZE) do |relation, index| relation.each_batch(of: RANGE_SIZE) do |batch| - range = relation.pluck('MIN(id)', 'MAX(id)').first - schedule = index * 5.minutes + range = batch.pluck('MIN(id)', 'MAX(id)').first + delay = index * 5.minutes - BackgroundMigrationWorker.perform_in(schedule, MIGRATION, range) + BackgroundMigrationWorker.perform_in(delay, MIGRATION, range) end end end diff --git a/db/schema.rb b/db/schema.rb index cd488630237..0f4b0c0c3b3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170820100558) do +ActiveRecord::Schema.define(version: 20170824162758) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -34,7 +34,7 @@ ActiveRecord::Schema.define(version: 20170820100558) do t.string "logo" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.text "description_html", null: false + t.text "description_html" t.integer "cached_markdown_version" end diff --git a/doc/api/groups.md b/doc/api/groups.md index 2b3d8e125c8..c2daa8bc029 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -2,7 +2,8 @@ ## 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: @@ -43,7 +44,8 @@ You can search for groups by name or path, see below. ## 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 @@ -109,7 +111,8 @@ Example response: ## 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 diff --git a/doc/install/azure/index.md b/doc/install/azure/index.md index 9842d99ed02..9cc4b56c932 100644 --- a/doc/install/azure/index.md +++ b/doc/install/azure/index.md @@ -10,7 +10,7 @@ like Ubuntu, Red Hat Enterprise Linux, and of course - GitLab! This means that y pre-configured GitLab VM and have your very own private GitLab up and running in around 30 minutes. Let's get started. -### Getting started +## Getting started First, you'll need an account on Azure. There are three ways to do this: @@ -25,7 +25,7 @@ This is a great way to try out Azure and cloud computing, and you can subscription gives you recurring Azure credits every month, so why not put those credits to use and try out GitLab right now? -### Working with Azure +## Working with Azure Once you have an Azure account, you can get started. Login to Azure using [portal.azure.com](https://portal.azure.com) and the first thing you will see is the Dashboard: @@ -35,7 +35,7 @@ Once you have an Azure account, you can get started. Login to Azure using The Dashboard gives you a quick overview of Azure resources, and from here you you can build VMs, create SQL Databases, author websites, and perform lots of other cloud tasks. -### Create New VM +## Create New VM The [Azure Marketplace][Azure-Marketplace] is an online store for pre-configured applications and services which have been optimized for the cloud by software vendors like GitLab, and both @@ -56,7 +56,7 @@ Click **"Create"** and you will be presented with the "Create virtual machine" b ![Azure - Create Virtual Machine - Basics](img/azure-create-virtual-machine-basics.png) -### Basics +## Basics The first items we need to configure are the basic settings of the underlying virtual machine: @@ -84,7 +84,7 @@ Here are the settings we've used: Check the settings you have entered, and then click **"OK"** when you're ready to proceed. -### Size +## Size Next, you need to choose the size of your VM - selecting features such as the number of CPU cores, the amount of RAM, the size of storage (and its speed), etc. @@ -108,7 +108,7 @@ free trial credits, you'll likely want to learn Go ahead and click your chosen size, then click **"Select"** when you're ready to proceed to the next step. -### Settings +## Settings On the next blade, you're asked to configure the Storage, Network and Extension settings. We've gone with the default settings as they're sufficient for test-driving GitLab, but please @@ -118,7 +118,7 @@ choose the settings which best meet your own requirements: Review the settings and then click **"OK"** when you're ready to proceed to the last step. -### Purchase +## Purchase The Purchase page is the last step and here you will be presented with the price per hour for your new VM. You'll be billed only for the VM itself (e.g. "Standard DS1 v2") because the @@ -131,7 +131,7 @@ previous steps, just click on any of the four steps to re-open them. When you have read and agreed to the terms of use and are ready to proceed, click **"Purchase"**. -### Deployment +## Deployment At this point, Azure will begin deploying your new VM. The deployment process will take a few minutes to complete, with progress displayed on the **"Deployment"** blade: @@ -146,7 +146,7 @@ on the Azure Dashboard (you may need to refresh the page): The new VM can also be accessed by clicking the `All resources` or `Virtual machines` icons in the Azure Portal sidebar navigation menu. -### Setup a domain name +## Setup a domain name The VM will have a public IP address (static by default), but Azure allows us to assign a friendly DNS name to the VM, so let's go ahead and do that. @@ -174,7 +174,7 @@ to make sure your VM is configured to use a _static_ public IP address (i.e. not or you will have to reconfigure the DNS `A` record each time Azure reassigns your VM a new public IP address. Read [IP address types and allocation methods in Azure][Azure-IP-Address-Types] to learn more. -### Let's open some ports! +## Let's open some ports! At this stage you should have a running and fully operational VM. However, none of the services on your VM (e.g. GitLab) will be publicly accessible via the internet until you have opened up the @@ -202,7 +202,7 @@ Next, click **"Add"**: ![Azure - Network security group - Inbound security rules - Add](img/azure-nsg-inbound-sec-rules-add-highlight.png) -#### Which ports to open? +### Which ports to open? Like all servers, our VM will be running many services. However, we want to open up the correct ports to enable public internet access to two services in particular: @@ -213,7 +213,7 @@ public access to the instance of GitLab running on our VM. allowing public access (with authentication) to remote terminal sessions _(you'll see why we need [SSH] access to our VM [later on in this tutorial](#maintaining-your-gitlab-instance))_ -#### Open HTTP on Port 80 +### Open HTTP on Port 80 In the **"Add inbound security rule"** blade, let's open port 80 so that our VM will accept HTTP connections: @@ -225,7 +225,7 @@ connections: 1. Make sure the `Action` is set to **Allow** 1. Click **"OK"** -#### Open SSH on Port 22 +### Open SSH on Port 22 Repeat the above process, adding a second Inbound security rule to open port 22, enabling our VM to accept [SSH] connections: diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 49c3b2278c7..e56427304a6 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -2,7 +2,7 @@ module API class Groups < Grape::API include PaginationParams - before { authenticate! } + before { authenticate_non_get! } helpers do params :optional_params_ce do @@ -47,16 +47,8 @@ module API use :pagination end get do - groups = if params[:owned] - current_user.owned_groups - elsif current_user.admin - Group.all - elsif params[:all_available] - GroupsFinder.new(current_user).execute - else - current_user.groups - end - + find_params = { all_available: params[:all_available], owned: params[:owned] } + groups = GroupsFinder.new(current_user, find_params).execute groups = groups.search(params[:search]) if params[:search].present? groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present? groups = groups.reorder(params[:order_by] => params[:sort]) diff --git a/lib/gitlab/file_finder.rb b/lib/gitlab/file_finder.rb index 093d9ed8092..10ffc345bd5 100644 --- a/lib/gitlab/file_finder.rb +++ b/lib/gitlab/file_finder.rb @@ -6,27 +6,48 @@ module Gitlab attr_reader :project, :ref + delegate :repository, to: :project + def initialize(project, ref) @project = project @ref = ref end def find(query) - blobs = project.repository.search_files_by_content(query, ref).first(BATCH_SIZE) - found_file_names = Set.new + by_content = find_by_content(query) - results = blobs.map do |blob| - blob = Gitlab::ProjectSearchResults.parse_search_result(blob) - found_file_names << blob.filename + already_found = Set.new(by_content.map(&:filename)) + by_filename = find_by_filename(query, except: already_found) - [blob.filename, blob] - end + (by_content + by_filename) + .sort_by(&:filename) + .map { |blob| [blob.filename, blob] } + end - project.repository.search_files_by_name(query, ref).first(BATCH_SIZE).each do |filename| - results << [filename, OpenStruct.new(ref: ref)] unless found_file_names.include?(filename) - end + private - results.sort_by(&:first) + def find_by_content(query) + results = repository.search_files_by_content(query, ref).first(BATCH_SIZE) + results.map { |result| Gitlab::ProjectSearchResults.parse_search_result(result) } + end + + def find_by_filename(query, except: []) + filenames = repository.search_files_by_name(query, ref).first(BATCH_SIZE) + filenames.delete_if { |filename| except.include?(filename) } unless except.empty? + + blob_refs = filenames.map { |filename| [ref, filename] } + blobs = Gitlab::Git::Blob.batch(repository, blob_refs, blob_size_limit: 1024) + + blobs.map do |blob| + Gitlab::SearchResults::FoundBlob.new( + id: blob.id, + filename: blob.path, + basename: File.basename(blob.path), + ref: ref, + startline: 1, + data: blob.data + ) + end end end end diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb index 28b636f9359..c470cb7c716 100644 --- a/spec/features/issues/issue_detail_spec.rb +++ b/spec/features/issues/issue_detail_spec.rb @@ -40,4 +40,18 @@ feature 'Issue Detail', :js do end end end + + context 'when authored by a user who is later deleted' do + before do + issue.update_attribute(:author_id, nil) + sign_in(user) + visit project_issue_path(project, issue) + end + + it 'shows the issue' do + page.within('.issuable-details') do + expect(find('h2')).to have_content(issue.title) + end + end + end end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 6742d77937f..31d509455ba 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -281,4 +281,30 @@ describe "Search" do expect(page).to have_selector('.commit-row-description', count: 9) 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 diff --git a/spec/helpers/button_helper_spec.rb b/spec/helpers/button_helper_spec.rb index 250ba239033..4423560ecaa 100644 --- a/spec/helpers/button_helper_spec.rb +++ b/spec/helpers/button_helper_spec.rb @@ -62,4 +62,67 @@ describe ButtonHelper do 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 diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js index 867322ce8ae..8c68ceff914 100644 --- a/spec/javascripts/api_spec.js +++ b/spec/javascripts/api_spec.js @@ -17,7 +17,7 @@ describe('Api', () => { beforeEach(() => { originalGon = window.gon; - window.gon = dummyGon; + window.gon = Object.assign({}, dummyGon); }); afterEach(() => { @@ -98,10 +98,11 @@ describe('Api', () => { }); describe('projects', () => { - it('fetches projects', (done) => { + it('fetches projects with membership when logged in', (done) => { const query = 'dummy query'; const options = { unused: 'option' }; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`; + window.gon.current_user_id = 1; const expectedData = Object.assign({ search: query, per_page: 20, @@ -119,6 +120,27 @@ describe('Api', () => { 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', () => { diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index cc336180ff7..3d36bb3e4d4 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -7,6 +7,7 @@ import '~/project_select'; import '~/project'; describe('Project Title', () => { + const dummyApiVersion = 'v3000'; preloadFixtures('issues/open-issue.html.raw'); loadJSONFixtures('projects.json'); @@ -14,7 +15,7 @@ describe('Project Title', () => { loadFixtures('issues/open-issue.html.raw'); window.gon = {}; - window.gon.api_version = 'v3'; + window.gon.api_version = dummyApiVersion; // eslint-disable-next-line no-new new Project(); @@ -37,9 +38,10 @@ describe('Project Title', () => { 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/v3/projects.json?simple=true'); + expect(reqUrl).toBe(`/api/${dummyApiVersion}/projects.json?simple=true`); expect(reqData).toEqual({ search: '', order_by: 'last_activity_at', diff --git a/spec/lib/gitlab/file_finder_spec.rb b/spec/lib/gitlab/file_finder_spec.rb index 3fb6315a39a..07cb10e563e 100644 --- a/spec/lib/gitlab/file_finder_spec.rb +++ b/spec/lib/gitlab/file_finder_spec.rb @@ -7,15 +7,23 @@ describe Gitlab::FileFinder do it 'finds by name' do results = finder.find('files') - expect(results.map(&:first)).to include('files/images/wm.svg') + + filename, blob = results.find { |_, blob| blob.filename == 'files/images/wm.svg' } + expect(filename).to eq('files/images/wm.svg') + expect(blob).to be_a(Gitlab::SearchResults::FoundBlob) + expect(blob.ref).to eq(finder.ref) + expect(blob.data).not_to be_empty end it 'finds by content' do results = finder.find('files') - blob = results.select { |result| result.first == "CHANGELOG" }.flatten.last + filename, blob = results.find { |_, blob| blob.filename == 'CHANGELOG' } - expect(blob.filename).to eq("CHANGELOG") + expect(filename).to eq('CHANGELOG') + expect(blob).to be_a(Gitlab::SearchResults::FoundBlob) + expect(blob.ref).to eq(finder.ref) + expect(blob.data).not_to be_empty end end end diff --git a/spec/migrations/migrate_stages_statuses_spec.rb b/spec/migrations/migrate_stages_statuses_spec.rb index 4102d57e368..094c9bc604e 100644 --- a/spec/migrations/migrate_stages_statuses_spec.rb +++ b/spec/migrations/migrate_stages_statuses_spec.rb @@ -12,7 +12,7 @@ describe MigrateStagesStatuses, :migration do before do 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: 2, name: 'gitlab2', path: 'gitlab2') @@ -50,9 +50,10 @@ describe MigrateStagesStatuses, :migration do Timecop.freeze do 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(BackgroundMigrationWorker.jobs.size).to eq 2 + expect(BackgroundMigrationWorker.jobs.size).to eq 3 end end end diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index 74c9d6145e2..586d073eb5e 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -38,6 +38,17 @@ describe Ci::Stage, :models do expect(stage.status).to eq 'success' end end + + context 'when stage status is not defined' do + before do + stage.update_column(:status, nil) + end + + it 'sets the default value' do + expect(described_class.find(stage.id).status) + .to eq 'created' + end + end end describe 'update_status' do diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 313c38cd29c..a7557c7fb22 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -20,10 +20,15 @@ describe API::Groups do describe "GET /groups" do context "when unauthenticated" do - it "returns authentication error" do + it "returns public groups" do 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 @@ -165,6 +170,18 @@ describe API::Groups do end 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 it "returns one of user1's groups" do project = create(:project, namespace: group2, path: 'Foo') diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index ac0aaa524b7..01aca74274c 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -45,18 +45,4 @@ module ApiHelpers oauth_access_token: oauth_access_token ) end - - def ci_api(path, user = nil) - "/ci/api/v1/#{path}" + - - # Normalize query string - (path.index('?') ? '' : '?') + - - # Append private_token if given a User object - if user.respond_to?(:private_token) - "&private_token=#{user.private_token}" - else - '' - end - end end |