From 819c14ecb5193519bbcd1bcea4eada7500dacf49 Mon Sep 17 00:00:00 2001 From: Praveen Arimbrathodiyil Date: Thu, 24 Nov 2016 13:41:30 +0530 Subject: [PATCH] New upstream version 8.13.6+dfsg1 --- CHANGELOG.md | 38 ++++ Gemfile | 2 +- Gemfile.lock | 6 +- VERSION | 2 +- app/assets/javascripts/application.js | 25 ++- app/assets/javascripts/merge_request_tabs.js | 2 +- .../admin/application_settings_controller.rb | 2 +- app/controllers/application_controller.rb | 3 +- app/controllers/jwt_controller.rb | 4 +- .../projects/commits_controller.rb | 9 +- .../projects/git_http_client_controller.rb | 8 +- .../projects/git_http_controller.rb | 6 +- .../projects/group_links_controller.rb | 2 +- app/controllers/projects/labels_controller.rb | 2 +- .../projects/lfs_api_controller.rb | 4 - .../projects/merge_requests_controller.rb | 15 +- app/controllers/projects/tags_controller.rb | 2 +- app/controllers/projects_controller.rb | 3 +- app/controllers/users_controller.rb | 3 +- app/finders/issuable_finder.rb | 37 ++-- app/finders/labels_finder.rb | 47 +++-- app/helpers/application_settings_helper.rb | 4 +- app/helpers/ci_status_helper.rb | 14 +- app/helpers/commits_helper.rb | 8 +- app/helpers/events_helper.rb | 4 +- app/helpers/lfs_helper.rb | 8 +- app/helpers/projects_helper.rb | 6 +- app/models/application_setting.rb | 35 +++- app/models/commit.rb | 15 +- app/models/concerns/issuable.rb | 6 +- .../project_features_compatibility.rb | 2 +- app/models/event.rb | 9 +- app/models/group_label.rb | 4 + app/models/guest.rb | 7 + app/models/issue.rb | 52 +++--- app/models/label.rb | 30 +-- app/models/merge_request.rb | 4 +- app/models/project.rb | 47 ++++- app/models/project_feature.rb | 14 +- app/models/project_label.rb | 4 + app/models/repository.rb | 24 +-- app/policies/issue_policy.rb | 4 + app/policies/project_policy.rb | 12 +- ...ntainer_registry_authentication_service.rb | 18 +- app/services/delete_branch_service.rb | 2 +- app/services/delete_tag_service.rb | 2 +- app/services/git_tag_push_service.rb | 4 +- .../application_settings/_form.html.haml | 4 +- app/views/devise/shared/_tabs_ldap.html.haml | 3 + app/views/groups/labels/index.html.haml | 2 +- app/views/projects/_last_commit.html.haml | 10 +- app/views/projects/blob/_blob.html.haml | 2 +- app/views/projects/branches/_branch.html.haml | 2 +- app/views/projects/commits/_commit.html.haml | 11 +- .../projects/commits/_commit_list.html.haml | 2 +- app/views/projects/commits/_commits.html.haml | 6 +- app/views/projects/commits/show.html.haml | 2 +- .../issues/_related_branches.html.haml | 2 +- app/views/projects/labels/index.html.haml | 4 +- .../merge_requests/branch_from.html.haml | 3 +- .../merge_requests/branch_to.html.haml | 3 +- .../merge_requests/show/_commits.html.haml | 2 +- app/views/projects/new.html.haml | 3 +- app/views/projects/show.html.haml | 2 +- app/views/projects/tags/_tag.html.haml | 2 +- app/views/search/results/_commit.html.haml | 2 +- app/views/shared/_label.html.haml | 13 +- app/views/shared/issuable/_form.html.haml | 1 + .../issuable/_milestone_dropdown.html.haml | 4 +- ...171205_rename_repository_storage_column.rb | 29 +++ db/schema.rb | 4 +- .../img/repository_storages_admin_ui.png | Bin 17081 -> 54043 bytes doc/administration/repository_storages.md | 3 + doc/api/settings.md | 4 +- doc/development/what_requires_downtime.md | 6 + lib/api/entities.rb | 5 +- lib/api/helpers.rb | 10 +- lib/api/labels.rb | 6 +- lib/api/settings.rb | 4 +- lib/banzai/filter/autolink_filter.rb | 38 +++- lib/banzai/reference_parser/base_parser.rb | 16 +- lib/banzai/reference_parser/commit_parser.rb | 6 + .../reference_parser/commit_range_parser.rb | 6 + .../reference_parser/external_issue_parser.rb | 6 + lib/banzai/reference_parser/label_parser.rb | 6 + .../reference_parser/merge_request_parser.rb | 6 + .../reference_parser/milestone_parser.rb | 6 + lib/banzai/reference_parser/snippet_parser.rb | 6 + lib/banzai/reference_parser/user_parser.rb | 34 +++- lib/banzai/renderer.rb | 6 +- lib/gitlab/contributions_calendar.rb | 74 +++++--- lib/gitlab/data_builder/push.rb | 2 +- lib/gitlab/git_access.rb | 91 +++++---- lib/gitlab/o_auth/user.rb | 2 + lib/gitlab/utils.rb | 8 + .../merge_requests_controller_spec.rb | 25 +++ spec/factories/projects.rb | 10 +- spec/features/commits_spec.rb | 23 ++- spec/features/groups/issues_spec.rb | 8 + spec/features/groups/merge_requests_spec.rb | 8 + .../issues/filter_by_milestone_spec.rb | 5 + .../features/issues/new_branch_button_spec.rb | 1 + spec/features/merge_requests/edit_mr_spec.rb | 14 +- .../projects/features_visibility_spec.rb | 16 ++ spec/features/projects/new_project_spec.rb | 19 ++ spec/features/projects/ref_switcher_spec.rb | 16 +- .../user_views_wiki_in_project_page_spec.rb | 44 +++++ spec/finders/branches_finder_spec.rb | 2 +- spec/finders/labels_finder_spec.rb | 15 ++ spec/finders/tags_finder_spec.rb | 2 +- .../lib/banzai/filter/autolink_filter_spec.rb | 22 +++ .../lib/banzai/filter/redactor_filter_spec.rb | 42 +++-- .../reference_parser/base_parser_spec.rb | 35 +--- .../reference_parser/commit_parser_spec.rb | 8 + .../commit_range_parser_spec.rb | 8 + .../external_issue_parser_spec.rb | 8 + .../reference_parser/issue_parser_spec.rb | 10 +- .../reference_parser/label_parser_spec.rb | 8 + .../merge_request_parser_spec.rb | 13 ++ .../reference_parser/milestone_parser_spec.rb | 8 + .../reference_parser/snippet_parser_spec.rb | 8 + .../reference_parser/user_parser_spec.rb | 2 + .../gitlab/closing_issue_extractor_spec.rb | 3 +- .../lib/gitlab/contributions_calendar_spec.rb | 104 +++++++++++ .../lib/gitlab/gfm/reference_rewriter_spec.rb | 2 +- spec/lib/gitlab/git_access_spec.rb | 33 ++++ spec/lib/gitlab/git_access_wiki_spec.rb | 2 +- spec/lib/gitlab/o_auth/user_spec.rb | 23 ++- spec/lib/gitlab/reference_extractor_spec.rb | 3 +- spec/lib/gitlab/utils_spec.rb | 35 ++++ spec/models/application_setting_spec.rb | 56 +++++- spec/models/commit_spec.rb | 51 +++++- spec/models/concerns/issuable_spec.rb | 5 + .../project_features_compatibility_spec.rb | 14 ++ spec/models/event_spec.rb | 32 +++- spec/models/guest_spec.rb | 47 +++++ spec/models/issue_spec.rb | 116 +++++++----- spec/models/project_spec.rb | 21 ++- spec/models/repository_spec.rb | 14 +- spec/policies/project_policy_spec.rb | 14 ++ spec/requests/api/api_helpers_spec.rb | 30 --- spec/requests/api/labels_spec.rb | 15 +- spec/requests/api/settings_spec.rb | 1 + spec/requests/git_http_spec.rb | 32 ++++ spec/requests/jwt_controller_spec.rb | 18 +- spec/requests/lfs_http_spec.rb | 12 +- ...er_registry_authentication_service_spec.rb | 28 ++- spec/services/git_tag_push_service_spec.rb | 173 +++++++++++++----- spec/services/projects/create_service_spec.rb | 2 +- spec/support/cycle_analytics_helpers.rb | 3 +- ...ures_apply_to_issuables_shared_examples.rb | 56 ++++++ .../reference_parser_shared_examples.rb | 43 +++++ .../_related_branches.html.haml_spec.rb | 2 +- 153 files changed, 1837 insertions(+), 563 deletions(-) create mode 100644 app/models/guest.rb create mode 100644 db/migrate/20161103171205_rename_repository_storage_column.rb create mode 100644 spec/features/groups/issues_spec.rb create mode 100644 spec/features/groups/merge_requests_spec.rb create mode 100644 spec/features/projects/new_project_spec.rb create mode 100644 spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb create mode 100644 spec/lib/gitlab/contributions_calendar_spec.rb create mode 100644 spec/lib/gitlab/utils_spec.rb create mode 100644 spec/models/guest_spec.rb create mode 100644 spec/support/project_features_apply_to_issuables_shared_examples.rb create mode 100644 spec/support/reference_parser_shared_examples.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index f47814ab3b..e11eb68240 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ Please view this file on the master branch, on stable branches it's out of date. +## 8.13.6 (2016-11-17) + +- Omniauth auto link LDAP user falls back to find by DN when user cannot be found by UID. !7002 +- Fix no "Register" tab if ldap auth is enabled (#24038). !7274 (Luc Didry) +- Fix cache for commit status in commits list to respect branches. !7372 +- Fix issue causing Labels not to appear in sidebar on MR page. !7416 (Alex Sanford) +- Limit labels returned for a specific project as an administrator. !7496 +- Clicking "force remove source branch" label now toggles the checkbox again. +- Allow commit note to be visible if repo is visible. +- Fix project Visibility Level selector not using default values. + +## 8.13.5 (2016-11-08) + +- Restore unauthenticated access to public container registries + +## 8.13.4 (2016-11-07) + +- Fix showing pipeline status for a given commit from correct branch. !7034 +- Only skip group when it's actually a group in the "Share with group" select. !7262 +- Introduce round-robin project creation to spread load over multiple shards. !7266 +- Ensure merge request's "remove branch" accessors return booleans. !7267 +- Ensure external users are not able to clone disabled repositories. +- Fix XSS issue in Markdown autolinker. +- Respect event visibility in Gitlab::ContributionsCalendar. +- Honour issue and merge request visibility in their respective finders. +- Disable reference Markdown for unavailable features. +- Fix lightweight tags not processed correctly by GitTagPushService. !6532 +- Allow owners to fetch source code in CI builds. !6943 +- Return conflict error in label API when title is taken by group label. !7014 +- Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project. !7123 +- Fix builds tab visibility. !7178 +- Fix project features default values. !7181 + +## 8.13.3 (2016-11-02) + +- Removes any symlinks before importing a project export file. CVE-2016-9086 +- Fixed Import/Export foreign key issue to do with project members. + ## 8.13.2 (2016-10-31) - Fix encoding issues on pipeline commits. !6832 diff --git a/Gemfile b/Gemfile index 46245ab62d..5a95fdc82a 100644 --- a/Gemfile +++ b/Gemfile @@ -51,7 +51,7 @@ gem 'browser', '~> 2.2' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem 'gitlab_git', '~> 10.6.8' +gem 'gitlab_git', '~> 10.7.0' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes diff --git a/Gemfile.lock b/Gemfile.lock index 442184b922..26c4dc0d23 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -283,7 +283,7 @@ GEM mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) gitlab-markup (1.5.0) - gitlab_git (10.6.8) + gitlab_git (10.7.0) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) @@ -867,7 +867,7 @@ DEPENDENCIES github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.0) - gitlab_git (~> 10.6.8) + gitlab_git (~> 10.7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.2) @@ -994,4 +994,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.13.2 + 1.13.5 diff --git a/VERSION b/VERSION index 3b3df40190..6b63235a86 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.13.3 +8.13.6 diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 8a61669822..58f7adf276 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -127,19 +127,30 @@ return $(document).off('scroll'); }; - window.shiftWindow = function() { - return scrollBy(0, -100); - }; - document.addEventListener("page:fetch", unbindEvents); - window.addEventListener("hashchange", shiftWindow); + // automatically adjust scroll position for hash urls taking the height of the navbar into account + // https://github.com/twitter/bootstrap/issues/1768 + window.adjustScroll = function() { + var navbar = document.querySelector('.navbar-gitlab'); + var subnav = document.querySelector('.layout-nav'); + var fixedTabs = document.querySelector('.js-tabs-affix'); - window.onload = function() { + adjustment = 0; + if (navbar) adjustment -= navbar.offsetHeight; + if (subnav) adjustment -= subnav.offsetHeight; + if (fixedTabs) adjustment -= fixedTabs.offsetHeight; + + return scrollBy(0, adjustment); + }; + + window.addEventListener("hashchange", adjustScroll); + + window.onload = function () { // Scroll the window to avoid the topnav bar // https://github.com/twitter/bootstrap/issues/1768 if (location.hash) { - return setTimeout(shiftWindow, 100); + return setTimeout(adjustScroll, 100); } }; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 3dde979185..d282b2da2d 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -129,7 +129,7 @@ MergeRequestTabs.prototype.scrollToElement = function(container) { var $el, navBarHeight; if (window.location.hash) { - navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); + navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + document.querySelector('.js-tabs-affix').offsetHeight; $el = $(container + " " + window.location.hash + ":not(.match)"); if ($el.length) { return $.scrollTo(container + " " + window.location.hash + ":not(.match)", { diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 6ef7cf0bae..86e808314f 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -116,8 +116,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :metrics_packet_size, :send_user_confirmation_email, :container_registry_token_expire_delay, - :repository_storage, :enabled_git_access_protocol, + repository_storages: [], restricted_visibility_levels: [], import_sources: [], disabled_oauth_sign_in_sources: [] diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 37600ed875..517ad4f03f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -192,9 +192,10 @@ class ApplicationController < ActionController::Base end # JSON for infinite scroll via Pager object - def pager_json(partial, count) + def pager_json(partial, count, locals = {}) html = render_to_string( partial, + locals: locals, layout: false, formats: [:html] ) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 7e4da73bc1..c736200a10 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -12,7 +12,7 @@ class JwtController < ApplicationController return head :not_found unless service result = service.new(@authentication_result.project, @authentication_result.actor, auth_params). - execute(authentication_abilities: @authentication_result.authentication_abilities || []) + execute(authentication_abilities: @authentication_result.authentication_abilities) render json: result, status: result[:http_status] end @@ -20,7 +20,7 @@ class JwtController < ApplicationController private def authenticate_project_or_user - @authentication_result = Gitlab::Auth::Result.new + @authentication_result = Gitlab::Auth::Result.new(nil, nil, :none, Gitlab::Auth.read_authentication_abilities) authenticate_with_http_basic do |login, password| @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip) diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index a52c614b25..4f38b2202f 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -26,8 +26,15 @@ class Projects::CommitsController < Projects::ApplicationController respond_to do |format| format.html - format.json { pager_json("projects/commits/_commits", @commits.size) } format.atom { render layout: false } + + format.json do + pager_json( + 'projects/commits/_commits', + @commits.size, + project: @project, + ref: @ref) + end end end end diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 383e184d79..3f41916e6d 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -21,10 +21,6 @@ class Projects::GitHttpClientController < Projects::ApplicationController def authenticate_user @authentication_result = Gitlab::Auth::Result.new - if project && project.public? && download_request? - return # Allow access - end - if allow_basic_auth? && basic_auth_provided? login, password = user_name_and_password(request) @@ -41,6 +37,10 @@ class Projects::GitHttpClientController < Projects::ApplicationController send_final_spnego_response return # Allow access end + elsif project && download_request? && Guest.can?(:download_code, project) + @authentication_result = Gitlab::Auth::Result.new(nil, project, :none, [:download_code]) + + return # Allow access end send_challenges diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 662d38b10a..13caeb42d4 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -78,11 +78,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController def upload_pack_allowed? return false unless Gitlab.config.gitlab_shell.upload_pack - if user - access_check.allowed? - else - ci? || project.public? - end + access_check.allowed? || ci? end def access diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index ae060abee5..9eaf26a0db 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -7,7 +7,7 @@ class Projects::GroupLinksController < Projects::ApplicationController @group_links = project.project_group_links.all @skip_groups = @group_links.pluck(:group_id) - @skip_groups << project.group.try(:id) + @skip_groups << project.namespace_id unless project.personal? end def create diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 4f85513436..42fd09e9b7 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -126,7 +126,7 @@ class Projects::LabelsController < Projects::ApplicationController alias_method :subscribable_resource, :label def find_labels - @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute.includes(:priorities) + @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute end def authorize_admin_labels! diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb index ece49dcd92..2d49327694 100644 --- a/app/controllers/projects/lfs_api_controller.rb +++ b/app/controllers/projects/lfs_api_controller.rb @@ -31,10 +31,6 @@ class Projects::LfsApiController < Projects::GitHttpClientController private - def objects - @objects ||= (params[:objects] || []).to_a - end - def existing_oids @existing_oids ||= begin storage_project.lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 2ee53f7ced..6e15c06c12 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -352,13 +352,23 @@ class Projects::MergeRequestsController < Projects::ApplicationController def branch_from # This is always source @source_project = @merge_request.nil? ? @project : @merge_request.source_project - @commit = @repository.commit(params[:ref]) if params[:ref].present? + + if params[:ref].present? + @ref = params[:ref] + @commit = @repository.commit(@ref) + end + render layout: false end def branch_to @target_project = selected_target_project - @commit = @target_project.commit(params[:ref]) if params[:ref].present? + + if params[:ref].present? + @ref = params[:ref] + @commit = @target_project.commit(@ref) + end + render layout: false end @@ -497,6 +507,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.close end + labels define_pipelines_vars end diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 8fea20cefe..953091492a 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -23,7 +23,7 @@ class Projects::TagsController < Projects::ApplicationController return render_404 unless @tag @release = @project.releases.find_or_initialize_by(tag: @tag.name) - @commit = @repository.commit(@tag.target) + @commit = @repository.commit(@tag.dereferenced_target) end def create diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 76b730198d..fbe93e5dda 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -287,7 +287,8 @@ class ProjectsController < Projects::ApplicationController render 'projects/empty' if @project.empty_repo? else if @project.wiki_enabled? - @wiki_home = @project.wiki.find_page('home', params[:version_id]) + @project_wiki = @project.wiki + @wiki_home = @project_wiki.find_page('home', params[:version_id]) elsif @project.feature_available?(:issues, current_user) @issues = issues_collection @issues = @issues.page(params[:page]) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 6a881b271d..c4508ccc3b 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -104,8 +104,7 @@ class UsersController < ApplicationController end def contributions_calendar - @contributions_calendar ||= Gitlab::ContributionsCalendar. - new(contributed_projects, user) + @contributions_calendar ||= Gitlab::ContributionsCalendar.new(user, current_user) end def load_events diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index e27986ef95..6297b2db36 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -61,31 +61,26 @@ class IssuableFinder def project return @project if defined?(@project) - if project? - @project = Project.find(params[:project_id]) + project = Project.find(params[:project_id]) + project = nil unless Ability.allowed?(current_user, :"read_#{klass.to_ability_name}", project) - unless Ability.allowed?(current_user, :read_project, @project) - @project = nil - end - else - @project = nil - end - - @project + @project = project end def projects return @projects if defined?(@projects) + return @projects = project if project? - if project? - @projects = project - elsif current_user && params[:authorized_only].presence && !current_user_related? - @projects = current_user.authorized_projects.reorder(nil) - elsif group - @projects = GroupProjectsFinder.new(group).execute(current_user).reorder(nil) - else - @projects = ProjectsFinder.new.execute(current_user).reorder(nil) - end + projects = + if current_user && params[:authorized_only].presence && !current_user_related? + current_user.authorized_projects + elsif group + GroupProjectsFinder.new(group).execute(current_user) + else + ProjectsFinder.new.execute(current_user) + end + + @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) end def search @@ -126,7 +121,7 @@ class IssuableFinder @labels = if labels? && !filter_by_no_label? - LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute + LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute(skip_authorization: true) else Label.none end @@ -273,7 +268,7 @@ class IssuableFinder items = items.with_label(label_names, params[:sort]) if projects - label_ids = LabelsFinder.new(current_user, project_ids: projects).execute.select(:id) + label_ids = LabelsFinder.new(current_user, project_ids: projects).execute(skip_authorization: true).select(:id) items = items.where(labels: { id: label_ids }) end end diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 865f093f04..fa0e2a5e3d 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -6,7 +6,7 @@ class LabelsFinder < UnionFinder def execute(skip_authorization: false) @skip_authorization = skip_authorization - items = find_union(label_ids, Label) + items = find_union(label_ids, Label) || Label.none items = with_title(items) sort(items) end @@ -18,9 +18,11 @@ class LabelsFinder < UnionFinder def label_ids label_ids = [] - if project - label_ids << project.group.labels if project.group.present? - label_ids << project.labels + if project? + if project + label_ids << project.group.labels if project.group.present? + label_ids << project.labels + end else label_ids << Label.where(group_id: projects.group_ids) label_ids << Label.where(project_id: projects.select(:id)) @@ -40,16 +42,16 @@ class LabelsFinder < UnionFinder items.where(title: title) end - def group_id - params[:group_id].presence + def group? + params[:group_id].present? end - def project_id - params[:project_id].presence + def project? + params[:project_id].present? end - def projects_ids - params[:project_ids] + def projects? + params[:project_ids].present? end def title @@ -59,8 +61,9 @@ class LabelsFinder < UnionFinder def project return @project if defined?(@project) - if project_id - @project = find_project + if project? + @project = Project.find(params[:project_id]) + @project = nil unless authorized_to_read_labels?(@project) else @project = nil end @@ -68,26 +71,20 @@ class LabelsFinder < UnionFinder @project end - def find_project - if skip_authorization - Project.find_by(id: project_id) - else - available_projects.find_by(id: project_id) - end - end - def projects return @projects if defined?(@projects) - @projects = skip_authorization ? Project.all : available_projects - @projects = @projects.in_namespace(group_id) if group_id - @projects = @projects.where(id: projects_ids) if projects_ids + @projects = skip_authorization ? Project.all : ProjectsFinder.new.execute(current_user) + @projects = @projects.in_namespace(params[:group_id]) if group? + @projects = @projects.where(id: params[:project_ids]) if projects? @projects = @projects.reorder(nil) @projects end - def available_projects - @available_projects ||= ProjectsFinder.new.execute(current_user) + def authorized_to_read_labels?(project) + return true if skip_authorization + + Ability.allowed?(current_user, :read_label, project) end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 6229384817..45a567a1eb 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -93,11 +93,11 @@ module ApplicationSettingsHelper end end - def repository_storage_options_for_select + def repository_storages_options_for_select options = Gitlab.config.repositories.storages.map do |name, path| ["#{name} - #{path}", name] end - options_for_select(options, @application_setting.repository_storage) + options_for_select(options, @application_setting.repository_storages) end end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index b7f48630bd..3decedace4 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -54,10 +54,18 @@ module CiStatusHelper custom_icon(icon_name) end - def render_commit_status(commit, tooltip_placement: 'auto left') + def render_commit_status(commit, ref: nil, tooltip_placement: 'auto left') project = commit.project - path = pipelines_namespace_project_commit_path(project.namespace, project, commit) - render_status_with_link('commit', commit.status, path, tooltip_placement: tooltip_placement) + path = pipelines_namespace_project_commit_path( + project.namespace, + project, + commit) + + render_status_with_link( + 'commit', + commit.status(ref), + path, + tooltip_placement: tooltip_placement) end def render_pipeline_status(pipeline, tooltip_placement: 'auto left') diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 33dcee49ae..ed402b698f 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -25,9 +25,11 @@ module CommitsHelper end end - def commit_to_html(commit, project, inline = true) - template = inline ? "inline_commit" : "commit" - render "projects/commits/#{template}", commit: commit, project: project unless commit.nil? + def commit_to_html(commit, ref, project) + render 'projects/commits/commit', + commit: commit, + ref: ref, + project: project end # Breadcrumb links for a Project and, if applicable, a tree path diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index f8ded05c31..6d8b8b5dfa 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -80,7 +80,7 @@ module EventsHelper elsif event.merge_request? namespace_project_merge_request_url(event.project.namespace, event.project, event.merge_request) - elsif event.note? && event.commit_note? + elsif event.commit_note? namespace_project_commit_url(event.project.namespace, event.project, event.note_target) elsif event.note? @@ -121,7 +121,7 @@ module EventsHelper end def event_note_target_path(event) - if event.note? && event.commit_note? + if event.commit_note? namespace_project_commit_path(event.project.namespace, event.project, event.note_target, diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb index 95b60aeab5..2425c3a8bc 100644 --- a/app/helpers/lfs_helper.rb +++ b/app/helpers/lfs_helper.rb @@ -1,6 +1,6 @@ module LfsHelper include Gitlab::Routing.url_helpers - + def require_lfs_enabled! return if Gitlab.config.lfs.enabled @@ -27,7 +27,11 @@ module LfsHelper def lfs_download_access? return false unless project.lfs_enabled? - project.public? || ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code? + ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code? + end + + def objects + @objects ||= (params[:objects] || []).to_a end def user_can_download_code? diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index d26b4018be..42c00ec3cd 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -174,10 +174,14 @@ module ProjectsHelper nav_tabs << :merge_requests end - if can?(current_user, :read_build, project) + if can?(current_user, :read_pipeline, project) nav_tabs << :pipelines end + if can?(current_user, :read_build, project) + nav_tabs << :builds + end + if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project) nav_tabs << :container_registry end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index c99aa7772b..9b02a68a20 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -18,6 +18,7 @@ class ApplicationSetting < ActiveRecord::Base serialize :disabled_oauth_sign_in_sources, Array serialize :domain_whitelist, Array serialize :domain_blacklist, Array + serialize :repository_storages cache_markdown_field :sign_in_text cache_markdown_field :help_page_text @@ -74,9 +75,8 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { only_integer: true, greater_than: 0 } - validates :repository_storage, - presence: true, - inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } } + validates :repository_storages, presence: true + validate :check_repository_storages validates :enabled_git_access_protocol, inclusion: { in: %w(ssh http), allow_blank: true, allow_nil: true } @@ -166,7 +166,7 @@ class ApplicationSetting < ActiveRecord::Base disabled_oauth_sign_in_sources: [], send_user_confirmation_email: false, container_registry_token_expire_delay: 5, - repository_storage: 'default', + repository_storages: ['default'], user_default_external: false, ) end @@ -201,6 +201,25 @@ class ApplicationSetting < ActiveRecord::Base self.domain_blacklist_raw = file.read end + def repository_storages + Array(read_attribute(:repository_storages)) + end + + # repository_storage is still required in the API. Remove in 9.0 + def repository_storage + repository_storages.first + end + + def repository_storage=(value) + self.repository_storages = [value] + end + + # Choose one of the available repository storage options. Currently all have + # equal weighting. + def pick_repository_storage + repository_storages.sample + end + def runners_registration_token ensure_runners_registration_token! end @@ -208,4 +227,12 @@ class ApplicationSetting < ActiveRecord::Base def health_check_access_token ensure_health_check_access_token! end + + private + + def check_repository_storages + invalid = repository_storages - Gitlab.config.repositories.storages.keys + errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless + invalid.empty? + end end diff --git a/app/models/commit.rb b/app/models/commit.rb index e64fd1e0c1..9e7fde9503 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -226,12 +226,19 @@ class Commit end def pipelines - @pipeline ||= project.pipelines.where(sha: sha) + project.pipelines.where(sha: sha) end - def status - return @status if defined?(@status) - @status ||= pipelines.status + def status(ref = nil) + @statuses ||= {} + + if @statuses.key?(ref) + @statuses[ref] + elsif ref + @statuses[ref] = pipelines.where(ref: ref).status + else + @statuses[ref] = pipelines.status + end end def revert_branch_name diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 17c3b526c9..ce5f1deba2 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -182,6 +182,10 @@ module Issuable grouping_columns end + + def to_ability_name + model_name.singular + end end def today? @@ -243,7 +247,7 @@ module Issuable # issuable.class # => MergeRequest # issuable.to_ability_name # => "merge_request" def to_ability_name - self.class.to_s.underscore + self.class.to_ability_name end # Returns a Hash of attributes to be used for Twitter card metadata diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 9216122923..6d88951c71 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -31,7 +31,7 @@ module ProjectFeaturesCompatibility def write_feature_attribute(field, value) build_project_feature unless project_feature - access_level = value == "true" ? ProjectFeature::ENABLED : ProjectFeature::DISABLED + access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED project_feature.update_attribute(field, access_level) end end diff --git a/app/models/event.rb b/app/models/event.rb index 0764cb8cab..1d193de1d3 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -48,6 +48,7 @@ class Event < ActiveRecord::Base update_all(updated_at: Time.now) end + # Update Gitlab::ContributionsCalendar#activity_dates if this changes def contributions where("action = ? OR (target_type in (?) AND action in (?))", Event::PUSHED, ["MergeRequest", "Issue"], @@ -60,8 +61,8 @@ class Event < ActiveRecord::Base end def visible_to_user?(user = nil) - if push? - true + if push? || commit_note? + Ability.allowed?(user, :download_code, project) elsif membership_changed? true elsif created_project? @@ -275,7 +276,7 @@ class Event < ActiveRecord::Base end def commit_note? - target.for_commit? + note? && target && target.for_commit? end def issue_note? @@ -287,7 +288,7 @@ class Event < ActiveRecord::Base end def project_snippet_note? - target.for_snippet? + note? && target && target.for_snippet? end def note_target diff --git a/app/models/group_label.rb b/app/models/group_label.rb index a698b532d1..68841ace2e 100644 --- a/app/models/group_label.rb +++ b/app/models/group_label.rb @@ -5,6 +5,10 @@ class GroupLabel < Label alias_attribute :subject, :group + def subject_foreign_key + 'group_id' + end + def to_reference(source_project = nil, target_project = nil, format: :id) super(source_project, target_project, format: format) end diff --git a/app/models/guest.rb b/app/models/guest.rb new file mode 100644 index 0000000000..01285ca126 --- /dev/null +++ b/app/models/guest.rb @@ -0,0 +1,7 @@ +class Guest + class << self + def can?(action, subject) + Ability.allowed?(nil, action, subject) + end + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index 133a599381..f0fe32187e 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -245,31 +245,11 @@ class Issue < ActiveRecord::Base # Returns `true` if the current issue can be viewed by either a logged in User # or an anonymous user. def visible_to_user?(user = nil) + return false unless project.feature_available?(:issues, user) + user ? readable_by?(user) : publicly_visible? end - # Returns `true` if the given User can read the current Issue. - def readable_by?(user) - if user.admin? - true - elsif project.owner == user - true - elsif confidential? - author == user || - assignee == user || - project.team.member?(user, Gitlab::Access::REPORTER) - else - project.public? || - project.internal? && !user.external? || - project.team.member?(user) - end - end - - # Returns `true` if this Issue is visible to everybody. - def publicly_visible? - project.public? && !confidential? - end - def overdue? due_date.try(:past?) || false end @@ -290,4 +270,32 @@ class Issue < ActiveRecord::Base end end end + + private + + # Returns `true` if the given User can read the current Issue. + # + # This method duplicates the same check of issue_policy.rb + # for performance reasons, check commit: 002ad215818450d2cbbc5fa065850a953dc7ada8 + # Make sure to sync this method with issue_policy.rb + def readable_by?(user) + if user.admin? + true + elsif project.owner == user + true + elsif confidential? + author == user || + assignee == user || + project.team.member?(user, Gitlab::Access::REPORTER) + else + project.public? || + project.internal? && !user.external? || + project.team.member?(user) + end + end + + # Returns `true` if this Issue is visible to everybody. + def publicly_visible? + project.public? && !confidential? + end end diff --git a/app/models/label.rb b/app/models/label.rb index 149fd98ecb..d9287f2dc2 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -92,16 +92,23 @@ class Label < ActiveRecord::Base nil end - def open_issues_count(user = nil, project = nil) - issues_count(user, project_id: project.try(:id) || project_id, state: 'opened') + def open_issues_count(user = nil) + issues_count(user, state: 'opened') end - def closed_issues_count(user = nil, project = nil) - issues_count(user, project_id: project.try(:id) || project_id, state: 'closed') + def closed_issues_count(user = nil) + issues_count(user, state: 'closed') end - def open_merge_requests_count(user = nil, project = nil) - merge_requests_count(user, project_id: project.try(:id) || project_id, state: 'opened') + def open_merge_requests_count(user = nil) + params = { + subject_foreign_key => subject.id, + label_name: title, + scope: 'all', + state: 'opened' + } + + MergeRequestsFinder.new(user, params.with_indifferent_access).execute.count end def prioritize!(project, value) @@ -167,15 +174,8 @@ class Label < ActiveRecord::Base end def issues_count(user, params = {}) - IssuesFinder.new(user, params.reverse_merge(label_name: title, scope: 'all')) - .execute - .count - end - - def merge_requests_count(user, params = {}) - MergeRequestsFinder.new(user, params.reverse_merge(label_name: title, scope: 'all')) - .execute - .count + params.merge!(subject_foreign_key => subject.id, label_name: title, scope: 'all') + IssuesFinder.new(user, params.with_indifferent_access).execute.count end def label_format_reference(format = :id) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c476a3bb14..7663e055f0 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -442,11 +442,11 @@ class MergeRequest < ActiveRecord::Base end def should_remove_source_branch? - merge_params['should_remove_source_branch'].present? + Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch']) end def force_remove_source_branch? - merge_params['force_remove_source_branch'].present? + Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch']) end def remove_source_branch? diff --git a/app/models/project.rb b/app/models/project.rb index a5c1d5c9e3..e0062f3dbb 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -28,8 +28,13 @@ class Project < ActiveRecord::Base default_value_for :archived, false default_value_for :visibility_level, gitlab_config_features.visibility_level default_value_for :container_registry_enabled, gitlab_config_features.container_registry - default_value_for(:repository_storage) { current_application_settings.repository_storage } + default_value_for(:repository_storage) { current_application_settings.pick_repository_storage } default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } + default_value_for :issues_enabled, gitlab_config_features.issues + default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests + default_value_for :builds_enabled, gitlab_config_features.builds + default_value_for :wiki_enabled, gitlab_config_features.wiki + default_value_for :snippets_enabled, gitlab_config_features.snippets after_create :ensure_dir_exist after_create :create_project_feature, unless: :project_feature @@ -202,8 +207,38 @@ class Project < ActiveRecord::Base scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) } - scope :with_builds_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0') } - scope :with_issues_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.issues_access_level IS NULL or project_features.issues_access_level > 0') } + scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') } + + # "enabled" here means "not disabled". It includes private features! + scope :with_feature_enabled, ->(feature) { + access_level_attribute = ProjectFeature.access_level_attribute(feature) + with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED] }) + } + + # Picks a feature where the level is exactly that given. + scope :with_feature_access_level, ->(feature, level) { + access_level_attribute = ProjectFeature.access_level_attribute(feature) + with_project_feature.where(project_features: { access_level_attribute => level }) + } + + scope :with_builds_enabled, -> { with_feature_enabled(:builds) } + scope :with_issues_enabled, -> { with_feature_enabled(:issues) } + + # project features may be "disabled", "internal" or "enabled". If "internal", + # they are only available to team members. This scope returns projects where + # the feature is either enabled, or internal with permission for the user. + def self.with_feature_available_for_user(feature, user) + return with_feature_enabled(feature) if user.try(:admin?) + + unconditional = with_feature_access_level(feature, [nil, ProjectFeature::ENABLED]) + return unconditional if user.nil? + + conditional = with_feature_access_level(feature, ProjectFeature::PRIVATE) + authorized = user.authorized_projects.merge(conditional.reorder(nil)) + + union = Gitlab::SQL::Union.new([unconditional.select(:id), authorized.select(:id)]) + where(arel_table[:id].in(Arel::Nodes::SqlLiteral.new(union.to_sql))) + end scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') } scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) } @@ -390,7 +425,7 @@ class Project < ActiveRecord::Base end def group_ids - joins(:namespace).where(namespaces: { type: 'Group' }).pluck(:namespace_id) + joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id) end end @@ -1062,10 +1097,6 @@ class Project < ActiveRecord::Base forks.count end - def find_label(name) - labels.find_by(name: name) - end - def origin_merge_requests merge_requests.where(source_project_id: self.id) end diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index b37ce1d3cf..34fd5a57b5 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -20,6 +20,15 @@ class ProjectFeature < ActiveRecord::Base FEATURES = %i(issues merge_requests wiki snippets builds repository) + class << self + def access_level_attribute(feature) + feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name) + raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature) + + "#{feature}_access_level".to_sym + end + end + # Default scopes force us to unscope here since a service may need to check # permissions for a project in pending_delete # http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to @@ -35,9 +44,8 @@ class ProjectFeature < ActiveRecord::Base default_value_for :repository_access_level, value: ENABLED, allows_nil: false def feature_available?(feature, user) - raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature) - - get_permission(user, public_send("#{feature}_access_level")) + access_level = public_send(ProjectFeature.access_level_attribute(feature)) + get_permission(user, access_level) end def builds_enabled? diff --git a/app/models/project_label.rb b/app/models/project_label.rb index 33c2b61771..82f47f0e8f 100644 --- a/app/models/project_label.rb +++ b/app/models/project_label.rb @@ -12,6 +12,10 @@ class ProjectLabel < Label alias_attribute :subject, :project + def subject_foreign_key + 'project_id' + end + def to_reference(target_project = nil, format: :id) super(project, target_project, format: format) end diff --git a/app/models/repository.rb b/app/models/repository.rb index b6653d1853..fe4b73e9aa 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -178,7 +178,7 @@ class Repository before_remove_branch branch = find_branch(branch_name) - oldrev = branch.try(:target).try(:id) + oldrev = branch.try(:dereferenced_target).try(:id) newrev = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name @@ -294,10 +294,10 @@ class Repository # Rugged seems to throw a `ReferenceError` when given branch_names rather # than SHA-1 hashes number_commits_behind = raw_repository. - count_commits_between(branch.target.sha, root_ref_hash) + count_commits_between(branch.dereferenced_target.sha, root_ref_hash) number_commits_ahead = raw_repository. - count_commits_between(root_ref_hash, branch.target.sha) + count_commits_between(root_ref_hash, branch.dereferenced_target.sha) { behind: number_commits_behind, ahead: number_commits_ahead } end @@ -679,11 +679,11 @@ class Repository branches.sort_by(&:name) when 'updated_desc' branches.sort do |a, b| - commit(b.target).committed_date <=> commit(a.target).committed_date + commit(b.dereferenced_target).committed_date <=> commit(a.dereferenced_target).committed_date end when 'updated_asc' branches.sort do |a, b| - commit(a.target).committed_date <=> commit(b.target).committed_date + commit(a.dereferenced_target).committed_date <=> commit(b.dereferenced_target).committed_date end else branches @@ -858,7 +858,7 @@ class Repository branch = find_branch(ref) if branch - last_commit = branch.target + last_commit = branch.dereferenced_target index.read_tree(last_commit.raw_commit.tree) parents = [last_commit.sha] end @@ -945,7 +945,7 @@ class Repository end def revert(user, commit, base_branch, revert_tree_id = nil) - source_sha = find_branch(base_branch).target.sha + source_sha = find_branch(base_branch).dereferenced_target.sha revert_tree_id ||= check_revert_content(commit, base_branch) return false unless revert_tree_id @@ -962,7 +962,7 @@ class Repository end def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil) - source_sha = find_branch(base_branch).target.sha + source_sha = find_branch(base_branch).dereferenced_target.sha cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch) return false unless cherry_pick_tree_id @@ -991,7 +991,7 @@ class Repository end def check_revert_content(commit, base_branch) - source_sha = find_branch(base_branch).target.sha + source_sha = find_branch(base_branch).dereferenced_target.sha args = [commit.id, source_sha] args << { mainline: 1 } if commit.merge_commit? @@ -1005,7 +1005,7 @@ class Repository end def check_cherry_pick_content(commit, base_branch) - source_sha = find_branch(base_branch).target.sha + source_sha = find_branch(base_branch).dereferenced_target.sha args = [commit.id, source_sha] args << 1 if commit.merge_commit? @@ -1078,7 +1078,7 @@ class Repository if rugged.lookup(newrev).parent_ids.empty? || target_branch.nil? oldrev = Gitlab::Git::BLANK_SHA else - oldrev = rugged.merge_base(newrev, target_branch.target.sha) + oldrev = rugged.merge_base(newrev, target_branch.dereferenced_target.sha) end GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do @@ -1138,7 +1138,7 @@ class Repository end def tags_sorted_by_committed_date - tags.sort_by { |tag| tag.target.committed_date } + tags.sort_by { |tag| tag.dereferenced_target.committed_date } end def keep_around_ref_name(sha) diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb index bd1811a3c5..c7114b3eae 100644 --- a/app/policies/issue_policy.rb +++ b/app/policies/issue_policy.rb @@ -1,4 +1,8 @@ class IssuePolicy < IssuablePolicy + # This class duplicates the same check of Issue#readable_by? for performance reasons + # Make sure to sync this class checks with issue.rb to avoid security problems. + # Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information. + def issue @subject end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index fbb3d4507d..1ee31023e2 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -2,11 +2,11 @@ class ProjectPolicy < BasePolicy def rules team_access!(user) - owner = user.admin? || - project.owner == user || + owner = project.owner == user || (project.group && project.group.has_owner?(user)) - owner_access! if owner + owner_access! if user.admin? || owner + team_member_owner_access! if owner if project.public? || (project.internal? && !user.external?) guest_access! @@ -16,7 +16,7 @@ class ProjectPolicy < BasePolicy can! :read_build if project.public_builds? if project.request_access_enabled && - !(owner || project.team.member?(user) || project_group_member?(user)) + !(owner || user.admin? || project.team.member?(user) || project_group_member?(user)) can! :request_access end end @@ -135,6 +135,10 @@ class ProjectPolicy < BasePolicy can! :destroy_issue end + def team_member_owner_access! + team_member_reporter_access! + end + # Push abilities on the users team role def team_access!(user) access = project.team.max_member_access(user.id) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 8ea88da8a5..c00c5aebf5 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -9,8 +9,8 @@ module Auth return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled - unless current_user || project - return error('DENIED', status: 403, message: 'access forbidden') unless scope + unless scope || current_user || project + return error('DENIED', status: 403, message: 'access forbidden') end { token: authorized_token(scope).encoded } @@ -76,7 +76,7 @@ module Auth case requested_action when 'pull' - requested_project.public? || build_can_pull?(requested_project) || user_can_pull?(requested_project) + build_can_pull?(requested_project) || user_can_pull?(requested_project) when 'push' build_can_push?(requested_project) || user_can_push?(requested_project) else @@ -92,23 +92,23 @@ module Auth # Build can: # 1. pull from its own project (for ex. a build) # 2. read images from dependent projects if creator of build is a team member - @authentication_abilities.include?(:build_read_container_image) && + has_authentication_ability?(:build_read_container_image) && (requested_project == project || can?(current_user, :build_read_container_image, requested_project)) end def user_can_pull?(requested_project) - @authentication_abilities.include?(:read_container_image) && + has_authentication_ability?(:read_container_image) && can?(current_user, :read_container_image, requested_project) end def build_can_push?(requested_project) # Build can push only to the project from which it originates - @authentication_abilities.include?(:build_create_container_image) && + has_authentication_ability?(:build_create_container_image) && requested_project == project end def user_can_push?(requested_project) - @authentication_abilities.include?(:create_container_image) && + has_authentication_ability?(:create_container_image) && can?(current_user, :create_container_image, requested_project) end @@ -118,5 +118,9 @@ module Auth http_status: status } end + + def has_authentication_ability?(capability) + (@authentication_abilities || []).include?(capability) + end end end diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index 918eddaa53..3e5dd4ebb8 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -42,7 +42,7 @@ class DeleteBranchService < BaseService Gitlab::DataBuilder::Push.build( project, current_user, - branch.target.sha, + branch.dereferenced_target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", []) diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb index d0cb151a01..d824406cb4 100644 --- a/app/services/delete_tag_service.rb +++ b/app/services/delete_tag_service.rb @@ -36,7 +36,7 @@ class DeleteTagService < BaseService Gitlab::DataBuilder::Push.build( project, current_user, - tag.target.sha, + tag.dereferenced_target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", []) diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index e6002b03b9..20a4445bdd 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -27,8 +27,8 @@ class GitTagPushService < BaseService tag_name = Gitlab::Git.ref_name(params[:ref]) tag = project.repository.find_tag(tag_name) - if tag && tag.object_sha == params[:newrev] - commit = project.commit(tag.target) + if tag && tag.target == params[:newrev] + commit = project.commit(tag.dereferenced_target) commits = [commit].compact message = tag.message end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index c4c68cd789..28003e5f50 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -353,9 +353,9 @@ %fieldset %legend Repository Storage .form-group - = f.label :repository_storage, 'Storage path for new projects', class: 'control-label col-sm-2' + = f.label :repository_storages, 'Storage paths for new projects', class: 'control-label col-sm-2' .col-sm-10 - = f.select :repository_storage, repository_storage_options_for_select, {}, class: 'form-control' + = f.select :repository_storages, repository_storages_options_for_select, {include_hidden: false}, multiple: true, class: 'form-control' .help-block Manage repository storage paths. Learn more in the = succeed "." do diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml index 1e957f0935..aec1b31ce6 100644 --- a/app/views/devise/shared/_tabs_ldap.html.haml +++ b/app/views/devise/shared/_tabs_ldap.html.haml @@ -8,3 +8,6 @@ - if signin_enabled? %li = link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab' + - if signin_enabled? && signup_enabled? + %li + = link_to 'Register', '#register-pane', 'data-toggle' => 'tab' diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index 70783a6340..45325d6bc4 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -13,7 +13,7 @@ .other-labels - if @labels.present? %ul.content-list.manage-labels-list.js-other-labels - = render partial: 'shared/label', collection: @labels, as: :label + = render partial: 'shared/label', subject: @group, collection: @labels, as: :label = paginate @labels, theme: 'gitlab' - else .nothing-here-block diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml index 630ae7d614..8e23d51b22 100644 --- a/app/views/projects/_last_commit.html.haml +++ b/app/views/projects/_last_commit.html.haml @@ -1,7 +1,9 @@ -- if commit.status - = link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{commit.status}" do - = ci_icon_for_status(commit.status) - = ci_label_for_status(commit.status) +- ref = local_assigns.fetch(:ref) +- status = commit.status(ref) +- if status + = link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do + = ci_icon_for_status(status) + = ci_label_for_status(status) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message" diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 3ffc3fcb7a..149ee7c59d 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -20,7 +20,7 @@ %ul.blob-commit-info.hidden-xs - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) - = render blob_commit, project: @project + = render blob_commit, project: @project, ref: @ref %div#blob-content-holder.blob-content-holder %article.file-holder diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 4480b2f22c..ed30ca7fbd 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -1,4 +1,4 @@ -- commit = @repository.commit(branch.target) +- commit = @repository.commit(branch.dereferenced_target) - bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0 - diverging_commit_counts = @repository.diverging_commit_counts(branch) - number_commits_behind = diverging_commit_counts[:behind] diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index fb48aef055..34855c5417 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -1,3 +1,4 @@ +- ref = local_assigns.fetch(:ref) - if @note_counts - note_count = @note_counts.fetch(commit.id, 0) - else @@ -5,7 +6,7 @@ - note_count = notes.user.count - cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count] -- cache_key.push(commit.status) if commit.status +- cache_key.push(commit.status(ref)) if commit.status(ref) = cache(cache_key, expires_in: 1.day) do %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } @@ -18,15 +19,15 @@ %span.commit-row-message.visible-xs-inline · = commit.short_id - - if commit.status + - if commit.status(ref) .visible-xs-inline - = render_commit_status(commit) + = render_commit_status(commit, ref: ref) - if commit.description? %a.text-expander.hidden-xs.js-toggle-button ... .commit-actions.hidden-xs - - if commit.status - = render_commit_status(commit) + - if commit.status(ref) + = render_commit_status(commit, ref: ref) = clipboard_button(clipboard_text: commit.id) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" = link_to_browse_code(project, commit) diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml index 46e4de4004..ce416caa49 100644 --- a/app/views/projects/commits/_commit_list.html.haml +++ b/app/views/projects/commits/_commit_list.html.haml @@ -11,4 +11,4 @@ %li.warning-row.unstyled #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. - else - %ul.content-list= render commits, project: @project + %ul.content-list= render commits, project: @project, ref: @ref diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index dd12eae8f7..48756c6894 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -1,13 +1,11 @@ -- unless defined?(project) - - project = @project - +- ref = local_assigns.fetch(:ref) - commits, hidden = limited_commits(@commits) - commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| %li.commit-header= "#{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}" %li.commits-row %ul.list-unstyled.commit-list - = render commits, project: project + = render commits, project: project, ref: ref - if hidden > 0 %li.alert.alert-warning diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 876c800262..9628cbd163 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -35,7 +35,7 @@ %div{id: dom_id(@project)} %ol#commits-list.list-unstyled.content_list - = render "commits", project: @project + = render 'commits', project: @project, ref: @ref = spinner :javascript diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index 44683c8bcd..1892ebb512 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -4,7 +4,7 @@ %ul.unstyled-list.related-merge-requests - @related_branches.each do |branch| %li - - target = @project.repository.find_branch(branch).target + - target = @project.repository.find_branch(branch).dereferenced_target - pipeline = @project.pipeline_for(branch, target.sha) if target - if pipeline %span.related-branch-ci-status diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index f135bf6f6b..05a8475dcd 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -22,14 +22,14 @@ %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) } %p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet - if @prioritized_labels.present? - = render partial: 'shared/label', collection: @prioritized_labels, as: :label + = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label .other-labels - if can?(current_user, :admin_label, @project) %h5{ class: ('hide' if hide) } Other Labels %ul.content-list.manage-labels-list.js-other-labels - if @labels.present? - = render partial: 'shared/label', collection: @labels, as: :label + = render partial: 'shared/label', subject: @project, collection: @labels, as: :label = paginate @labels, theme: 'gitlab' - if @labels.blank? .nothing-here-block diff --git a/app/views/projects/merge_requests/branch_from.html.haml b/app/views/projects/merge_requests/branch_from.html.haml index 4f90dde6fa..3837c4b388 100644 --- a/app/views/projects/merge_requests/branch_from.html.haml +++ b/app/views/projects/merge_requests/branch_from.html.haml @@ -1 +1,2 @@ -= commit_to_html(@commit, @source_project, false) +- if @commit + = commit_to_html(@commit, @ref, @source_project) diff --git a/app/views/projects/merge_requests/branch_to.html.haml b/app/views/projects/merge_requests/branch_to.html.haml index 67a7a6bcec..d69b71790a 100644 --- a/app/views/projects/merge_requests/branch_to.html.haml +++ b/app/views/projects/merge_requests/branch_to.html.haml @@ -1 +1,2 @@ -= commit_to_html(@commit, @target_project, false) +- if @commit + = commit_to_html(@commit, @ref, @target_project) diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index 0b05785430..a0e12fb3f3 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -3,4 +3,4 @@ Most recent commits displayed first %ol#commits-list.list-unstyled - = render "projects/commits/commits", project: @merge_request.project + = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 399ccf15b7..9436c64ee2 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -90,7 +90,8 @@ = f.label :visibility_level, class: 'label-light' do Visibility Level = link_to "(?)", help_page_path("public_access/public_access") - = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: @project.visibility_level, form_model: @project) + = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project + = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index ba16c64146..69a3bc5f04 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -79,7 +79,7 @@ = render 'shared/notifications/button', notification_setting: @notification_setting - if @repository.commit .project-last-commit{ class: container_class } - = render 'projects/last_commit', commit: @repository.commit, project: @project + = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project %div{ class: container_class } - if @project.archived? diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 05fccb4f97..c42641afea 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -1,4 +1,4 @@ -- commit = @repository.commit(tag.target) +- commit = @repository.commit(tag.dereferenced_target) - release = @releases.find { |release| release.tag == tag.name } %li %div diff --git a/app/views/search/results/_commit.html.haml b/app/views/search/results/_commit.html.haml index 5b2d83d6b9..f34eaf8902 100644 --- a/app/views/search/results/_commit.html.haml +++ b/app/views/search/results/_commit.html.haml @@ -1 +1 @@ -= render 'projects/commits/commit', project: @project, commit: commit += render 'projects/commits/commit', project: @project, commit: commit, ref: nil diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index 40c8d2af22..6ccdef0df4 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -1,6 +1,7 @@ - label_css_id = dom_id(label) -- open_issues_count = label.open_issues_count(current_user, @project) -- open_merge_requests_count = label.open_merge_requests_count(current_user, @project) +- open_issues_count = label.open_issues_count(current_user) +- open_merge_requests_count = label.open_merge_requests_count(current_user) +- subject = local_assigns[:subject] %li{id: label_css_id, data: { id: label.id } } = render "shared/label_row", label: label @@ -12,10 +13,10 @@ .dropdown-menu.dropdown-menu-align-right %ul %li - = link_to_label(label, subject: @project, type: :merge_request) do + = link_to_label(label, subject: subject, type: :merge_request) do = pluralize open_merge_requests_count, 'merge request' %li - = link_to_label(label, subject: @project) do + = link_to_label(label, subject: subject) do = pluralize open_issues_count, 'open issue' - if current_user %li.label-subscription{ data: toggle_subscription_data(label) } @@ -28,9 +29,9 @@ = link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, remote: true, data: {confirm: 'Remove this label? Are you sure?'} .pull-right.hidden-xs.hidden-sm.hidden-md - = link_to_label(label, subject: @project, type: :merge_request, css_class: 'btn btn-transparent btn-action') do + = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action') do = pluralize open_merge_requests_count, 'merge request' - = link_to_label(label, subject: @project, css_class: 'btn btn-transparent btn-action') do + = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action') do = pluralize open_issues_count, 'open issue' - if current_user diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index d410755cad..ce91b5966a 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -142,6 +142,7 @@ .col-sm-10.col-sm-offset-2 .checkbox = label_tag 'merge_request[force_remove_source_branch]' do + = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil = check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch? Remove source branch when merge request is accepted. diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index f27a9002ec..40fe53e6a8 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -1,10 +1,10 @@ - project = @target_project || @project - extra_class = extra_class || '' - show_menu_above = show_menu_above || false -- selected_text = selected.try(:title) +- selected_text = selected.try(:title) || params[:milestone_title] - dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone") - if selected.present? - = hidden_field_tag(name, name == :milestone_title ? selected.title : selected.id) + = hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id) = dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do - if project diff --git a/db/migrate/20161103171205_rename_repository_storage_column.rb b/db/migrate/20161103171205_rename_repository_storage_column.rb new file mode 100644 index 0000000000..9328057393 --- /dev/null +++ b/db/migrate/20161103171205_rename_repository_storage_column.rb @@ -0,0 +1,29 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RenameRepositoryStorageColumn < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = true + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + DOWNTIME_REASON = 'Renaming the application_settings.repository_storage column' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + rename_column :application_settings, :repository_storage, :repository_storages + end +end diff --git a/db/schema.rb b/db/schema.rb index 02282b0f66..a5f5f812f0 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: 20161024042317) do +ActiveRecord::Schema.define(version: 20161103171205) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -88,7 +88,7 @@ ActiveRecord::Schema.define(version: 20161024042317) do t.integer "container_registry_token_expire_delay", default: 5 t.text "after_sign_up_text" t.boolean "user_default_external", default: false, null: false - t.string "repository_storage", default: "default" + t.string "repository_storages", default: "default" t.string "enabled_git_access_protocol" t.boolean "domain_blacklist_enabled", default: false t.text "domain_blacklist" diff --git a/doc/administration/img/repository_storages_admin_ui.png b/doc/administration/img/repository_storages_admin_ui.png index 599350bc098052df2b51da8cb065f246463d8031..6481baca1ad90a76c3f09f554833a5fe9fb366d9 100644 GIT binary patch literal 54043 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 Gp$P!&esQ+| literal 17081 zcmb8X1ymegm-b5n!6mpufZ*=IEkJMx7Cczv?w&w!3+{~t2ri9laEIVDjT78m8o5pW z@Au6+cV_OayI3?(oIYK(>(n{>`R%7FTvb^X1C<074h{}O?!)_!aBwfSV87=hBf?&d zoZ5cCeju7C$i9brdj863D@uTEL2>+`;{peVhWq^e0xmt12(}T)RZdA7X%&e8?H#A~ zET0-2+#5K#_YxYO3x`=Ao_Lysp)25}i*K(bB_&bb|6uy~gGOH4TMa=$T%0j)GH$tm zv5jHo>IH#no~v33L(Ydd26DMCstMoC)ZIO{pr#2*FEh+=U%qu*-X2Uav$z@)*j{ex z5b!w4QsYsYdW|6Q8iC3vC*d`6;2Y#XWDxA-4Knq}^B-@zSpIEe7zf(FZTwe@e{KB# zc-#Nb*R~N(<*~`94nuSz&=&;FgxTlM6Lv(u5+z$a$EZkSsei(kmN@<+A7|2Ae4$~d z>){C$*Y^ABA$rv1PrVVZwvOe@2$s~Dt99(jTwRID9}U(Z1(a8Y1IU@!LLWm&ZRS?smf6 z?O9M_Q5zO~5%gCJUMnTXzxOv;-`;ADzZrs?yLQ-w+7T8{));(!U9nR|Ni2G
c* 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 C0zV7@ diff --git a/doc/administration/repository_storages.md b/doc/administration/repository_storages.md index 55b054fc1a..ab70557b69 100644 --- a/doc/administration/repository_storages.md +++ b/doc/administration/repository_storages.md @@ -91,6 +91,9 @@ be stored via the **Application Settings** in the Admin area. ![Choose repository storage path in Admin area](img/repository_storages_admin_ui.png) +Beginning with GitLab 8.13.4, multiple paths can be chosen. New projects will be +randomly placed on one of the selected paths. + [ce-4578]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4578 [restart gitlab]: restart_gitlab.md#installations-from-source [reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure diff --git a/doc/api/settings.md b/doc/api/settings.md index f7ad3b4cc8..218546aafe 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -42,6 +42,7 @@ Example response: "sign_in_text" : null, "container_registry_token_expire_delay": 5, "repository_storage": "default", + "repository_storages": ["default"], "koding_enabled": false, "koding_url": null } @@ -73,7 +74,8 @@ PUT /application/settings | `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider | | `after_sign_out_path` | string | no | Where to redirect users after logout | | `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes | -| `repository_storage` | string | no | Storage path for new projects. The value should be the name of one of the repository storage paths defined in your gitlab.yml | +| `repository_storages` | array of strings | no | A list of names of enabled storage paths, taken from `gitlab.yml`. New projects will be created in one of these stores, chosen at random. | +| `repository_storage` | string | no | The first entry in `repository_storages`. Deprecated, but retained for compatibility reasons | | `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. | | `koding_enabled` | boolean | no | Enable Koding integration. Default is `false`. | | `koding_url` | string | yes (if `koding_enabled` is `true`) | The Koding instance URL for integration. | diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md index 2574c2c047..bbcd26477f 100644 --- a/doc/development/what_requires_downtime.md +++ b/doc/development/what_requires_downtime.md @@ -66,6 +66,12 @@ producing errors whenever it tries to use the `dummy` column. As a result of the above downtime _is_ required when removing a column, even when using PostgreSQL. +## Renaming Columns + +Renaming columns requires downtime as running GitLab instances will continue +using the old column name until a new version is deployed. This can result +in the instance producing errors, which in turn can impact the user experience. + ## Changing Column Constraints Generally changing column constraints requires checking all rows in the table to diff --git a/lib/api/entities.rb b/lib/api/entities.rb index feaa0c213b..81ce5e807e 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -138,7 +138,7 @@ module API expose :name expose :commit do |repo_branch, options| - options[:project].repository.commit(repo_branch.target) + options[:project].repository.commit(repo_branch.dereferenced_target) end expose :protected do |repo_branch, options| @@ -510,6 +510,7 @@ module API expose :after_sign_out_path expose :container_registry_token_expire_delay expose :repository_storage + expose :repository_storages expose :koding_enabled expose :koding_url end @@ -523,7 +524,7 @@ module API expose :name, :message expose :commit do |repo_tag, options| - options[:project].repository.commit(repo_tag.target) + options[:project].repository.commit(repo_tag.dereferenced_target) end expose :release, using: Entities::Release do |repo_tag, options| diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 8025581d3c..3c9d7b1aae 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -1,18 +1,12 @@ module API module Helpers + include Gitlab::Utils + PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN" PRIVATE_TOKEN_PARAM = :private_token SUDO_HEADER = "HTTP_SUDO" SUDO_PARAM = :sudo - def to_boolean(value) - return value if [true, false].include?(value) - return true if value =~ /^(true|t|yes|y|1|on)$/i - return false if value =~ /^(false|f|no|n|0|off)$/i - - nil - end - def private_token params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER] end diff --git a/lib/api/labels.rb b/lib/api/labels.rb index 642e6345b9..40d7f4a515 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -29,8 +29,8 @@ module API required_attributes! [:name, :color] attrs = attributes_for_keys [:name, :color, :description] - label = user_project.find_label(attrs[:name]) + label = available_labels.find_by(title: attrs[:name]) conflict!('Label already exists') if label label = user_project.labels.create(attrs) @@ -54,7 +54,7 @@ module API authorize! :admin_label, user_project required_attributes! [:name] - label = user_project.find_label(params[:name]) + label = user_project.labels.find_by(title: params[:name]) not_found!('Label') unless label label.destroy @@ -75,7 +75,7 @@ module API authorize! :admin_label, user_project required_attributes! [:name] - label = user_project.find_label(params[:name]) + label = user_project.labels.find_by(title: params[:name]) not_found!('Label not found') unless label attrs = attributes_for_keys [:new_name, :color, :description] diff --git a/lib/api/settings.rb b/lib/api/settings.rb index c885fcd7ea..c4cb1c7924 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -17,12 +17,12 @@ module API present current_settings, with: Entities::ApplicationSetting end - # Modify applicaiton settings + # Modify application settings # # Example Request: # PUT /application/settings put "application/settings" do - attributes = current_settings.attributes.keys - ["id"] + attributes = ["repository_storage"] + current_settings.attributes.keys - ["id"] attrs = attributes_for_keys(attributes) if current_settings.update_attributes(attrs) diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb index 799b83b106..80c844baec 100644 --- a/lib/banzai/filter/autolink_filter.rb +++ b/lib/banzai/filter/autolink_filter.rb @@ -71,6 +71,14 @@ module Banzai @doc = parse_html(rinku) end + # Return true if any of the UNSAFE_PROTOCOLS strings are included in the URI scheme + def contains_unsafe?(scheme) + return false unless scheme + + scheme = scheme.strip.downcase + Banzai::Filter::SanitizationFilter::UNSAFE_PROTOCOLS.any? { |protocol| scheme.include?(protocol) } + end + # Autolinks any text matching LINK_PATTERN that Rinku didn't already # replace def text_parse @@ -89,17 +97,27 @@ module Banzai doc end - def autolink_filter(text) - text.gsub(LINK_PATTERN) do |match| - # Remove any trailing HTML entities and store them for appending - # outside the link element. The entity must be marked HTML safe in - # order to be output literally rather than escaped. - match.gsub!(/((?:&[\w#]+;)+)\z/, '') - dropped = ($1 || '').html_safe - - options = link_options.merge(href: match) - content_tag(:a, match, options) + dropped + def autolink_match(match) + # start by stripping out dangerous links + begin + uri = Addressable::URI.parse(match) + return match if contains_unsafe?(uri.scheme) + rescue Addressable::URI::InvalidURIError + return match end + + # Remove any trailing HTML entities and store them for appending + # outside the link element. The entity must be marked HTML safe in + # order to be output literally rather than escaped. + match.gsub!(/((?:&[\w#]+;)+)\z/, '') + dropped = ($1 || '').html_safe + + options = link_options.merge(href: match) + content_tag(:a, match, options) + dropped + end + + def autolink_filter(text) + text.gsub(LINK_PATTERN) { |match| autolink_match(match) } end def link_options diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb index f5d110e987..d8a855ec1f 100644 --- a/lib/banzai/reference_parser/base_parser.rb +++ b/lib/banzai/reference_parser/base_parser.rb @@ -63,12 +63,7 @@ module Banzai nodes.select do |node| if node.has_attribute?(project_attr) node_id = node.attr(project_attr).to_i - - if project && project.id == node_id - true - else - can?(user, :read_project, projects[node_id]) - end + can_read_reference?(user, projects[node_id]) else true end @@ -226,6 +221,15 @@ module Banzai attr_reader :current_user, :project + # When a feature is disabled or visible only for + # team members we should not allow team members + # see reference comments. + # Override this method on subclasses + # to check if user can read resource + def can_read_reference?(user, ref_project) + raise NotImplementedError + end + def lazy(&block) Gitlab::Lazy.new(&block) end diff --git a/lib/banzai/reference_parser/commit_parser.rb b/lib/banzai/reference_parser/commit_parser.rb index 0fee9d267d..8c54a041cb 100644 --- a/lib/banzai/reference_parser/commit_parser.rb +++ b/lib/banzai/reference_parser/commit_parser.rb @@ -29,6 +29,12 @@ module Banzai commits end + + private + + def can_read_reference?(user, ref_project) + can?(user, :download_code, ref_project) + end end end end diff --git a/lib/banzai/reference_parser/commit_range_parser.rb b/lib/banzai/reference_parser/commit_range_parser.rb index 69d01f8db1..0878b6afba 100644 --- a/lib/banzai/reference_parser/commit_range_parser.rb +++ b/lib/banzai/reference_parser/commit_range_parser.rb @@ -33,6 +33,12 @@ module Banzai range.valid_commits? ? range : nil end + + private + + def can_read_reference?(user, ref_project) + can?(user, :download_code, ref_project) + end end end end diff --git a/lib/banzai/reference_parser/external_issue_parser.rb b/lib/banzai/reference_parser/external_issue_parser.rb index a1264db211..6e7b766957 100644 --- a/lib/banzai/reference_parser/external_issue_parser.rb +++ b/lib/banzai/reference_parser/external_issue_parser.rb @@ -20,6 +20,12 @@ module Banzai def issue_ids_per_project(nodes) gather_attributes_per_project(nodes, self.class.data_attribute) end + + private + + def can_read_reference?(user, ref_project) + can?(user, :read_issue, ref_project) + end end end end diff --git a/lib/banzai/reference_parser/label_parser.rb b/lib/banzai/reference_parser/label_parser.rb index e5d1eb11d7..aa76c64ac5 100644 --- a/lib/banzai/reference_parser/label_parser.rb +++ b/lib/banzai/reference_parser/label_parser.rb @@ -6,6 +6,12 @@ module Banzai def references_relation Label end + + private + + def can_read_reference?(user, ref_project) + can?(user, :read_label, ref_project) + end end end end diff --git a/lib/banzai/reference_parser/merge_request_parser.rb b/lib/banzai/reference_parser/merge_request_parser.rb index c9a9ca79c0..40451947e6 100644 --- a/lib/banzai/reference_parser/merge_request_parser.rb +++ b/lib/banzai/reference_parser/merge_request_parser.rb @@ -6,6 +6,12 @@ module Banzai def references_relation MergeRequest.includes(:author, :assignee, :target_project) end + + private + + def can_read_reference?(user, ref_project) + can?(user, :read_merge_request, ref_project) + end end end end diff --git a/lib/banzai/reference_parser/milestone_parser.rb b/lib/banzai/reference_parser/milestone_parser.rb index a000ac61e5..d3968d6b22 100644 --- a/lib/banzai/reference_parser/milestone_parser.rb +++ b/lib/banzai/reference_parser/milestone_parser.rb @@ -6,6 +6,12 @@ module Banzai def references_relation Milestone end + + private + + def can_read_reference?(user, ref_project) + can?(user, :read_milestone, ref_project) + end end end end diff --git a/lib/banzai/reference_parser/snippet_parser.rb b/lib/banzai/reference_parser/snippet_parser.rb index fa71b3c952..63b592137b 100644 --- a/lib/banzai/reference_parser/snippet_parser.rb +++ b/lib/banzai/reference_parser/snippet_parser.rb @@ -6,6 +6,12 @@ module Banzai def references_relation Snippet end + + private + + def can_read_reference?(user, ref_project) + can?(user, :read_project_snippet, ref_project) + end end end end diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb index 863f5725d3..7adaffa19c 100644 --- a/lib/banzai/reference_parser/user_parser.rb +++ b/lib/banzai/reference_parser/user_parser.rb @@ -30,22 +30,36 @@ module Banzai nodes.each do |node| if node.has_attribute?(group_attr) - node_group = groups[node.attr(group_attr).to_i] - - if node_group && - can?(user, :read_group, node_group) - visible << node - end - # Remaining nodes will be processed by the parent class' - # implementation of this method. + next unless can_read_group_reference?(node, user, groups) + visible << node + elsif can_read_project_reference?(node) + visible << node else remaining << node end end + # If project does not belong to a group + # and does not have the same project id as the current project + # base class will check if user can read the project that contains + # the user reference. visible + super(current_user, remaining) end + # Check if project belongs to a group which + # user can read. + def can_read_group_reference?(node, user, groups) + node_group = groups[node.attr('data-group').to_i] + + node_group && can?(user, :read_group, node_group) + end + + def can_read_project_reference?(node) + node_id = node.attr('data-project').to_i + + project && project.id == node_id + end + def nodes_user_can_reference(current_user, nodes) project_attr = 'data-project' author_attr = 'data-author' @@ -88,6 +102,10 @@ module Banzai collection_objects_for_ids(Project, ids). flat_map { |p| p.team.members.to_a } end + + def can_read_reference?(user, ref_project) + can?(user, :read_project, ref_project) + end end end end diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index ce048a36fa..f31fb6c3f7 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -46,7 +46,7 @@ module Banzai return html if html.present? html = cacheless_render_field(object, field) - object.update_column(html_field, html) unless object.new_record? || object.destroyed? + update_object(object, html_field, html) unless object.new_record? || object.destroyed? html end @@ -166,5 +166,9 @@ module Banzai return unless cache_key Rails.cache.send(:expanded_key, full_cache_key(cache_key, pipeline_name)) end + + def update_object(object, html_field, html) + object.update_column(html_field, html) + end end end diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index b164f5a2ee..7e3d5647b3 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -1,45 +1,44 @@ module Gitlab class ContributionsCalendar - attr_reader :activity_dates, :projects, :user + attr_reader :contributor + attr_reader :current_user + attr_reader :projects - def initialize(projects, user) - @projects = projects - @user = user + def initialize(contributor, current_user = nil) + @contributor = contributor + @current_user = current_user + @projects = ContributedProjectsFinder.new(contributor).execute(current_user) end def activity_dates return @activity_dates if @activity_dates.present? - @activity_dates = {} + # Can't use Event.contributions here because we need to check 3 different + # project_features for the (currently) 3 different contribution types date_from = 1.year.ago + repo_events = event_counts(date_from, :repository). + having(action: Event::PUSHED) + issue_events = event_counts(date_from, :issues). + having(action: [Event::CREATED, Event::CLOSED], target_type: "Issue") + mr_events = event_counts(date_from, :merge_requests). + having(action: [Event::MERGED, Event::CREATED, Event::CLOSED], target_type: "MergeRequest") - events = Event.reorder(nil).contributions.where(author_id: user.id). - where("created_at > ?", date_from).where(project_id: projects). - group('date(created_at)'). - select('date(created_at) as date, count(id) as total_amount'). - map(&:attributes) + union = Gitlab::SQL::Union.new([repo_events, issue_events, mr_events]) + events = Event.find_by_sql(union.to_sql).map(&:attributes) - activity_dates = (1.year.ago.to_date..Date.today).to_a - - activity_dates.each do |date| - day_events = events.find { |day_events| day_events["date"] == date } - - if day_events - @activity_dates[date] = day_events["total_amount"] - end + @activity_events = events.each_with_object(Hash.new {|h, k| h[k] = 0 }) do |event, activities| + activities[event["date"]] += event["total_amount"] end - - @activity_dates end def events_by_date(date) - events = Event.contributions.where(author_id: user.id). - where("created_at > ? AND created_at < ?", date.beginning_of_day, date.end_of_day). + events = Event.contributions.where(author_id: contributor.id). + where(created_at: date.beginning_of_day..date.end_of_day). where(project_id: projects) - events.select do |event| - event.push? || event.issue? || event.merge_request? - end + # Use visible_to_user? instead of the complicated logic in activity_dates + # because we're only viewing the events for a single day. + events.select {|event| event.visible_to_user?(current_user) } end def starting_year @@ -49,5 +48,30 @@ module Gitlab def starting_month Date.today.month end + + private + + def event_counts(date_from, feature) + t = Event.arel_table + + # re-running the contributed projects query in each union is expensive, so + # use IN(project_ids...) instead. It's the intersection of two users so + # the list will be (relatively) short + @contributed_project_ids ||= projects.uniq.pluck(:id) + authed_projects = Project.where(id: @contributed_project_ids). + with_feature_available_for_user(feature, current_user). + reorder(nil). + select(:id) + + conditions = t[:created_at].gteq(date_from.beginning_of_day). + and(t[:created_at].lteq(Date.today.end_of_day)). + and(t[:author_id].eq(contributor.id)) + + Event.reorder(nil). + select(t[:project_id], t[:target_type], t[:action], 'date(created_at) AS date', 'count(id) as total_amount'). + group(t[:project_id], t[:target_type], t[:action], 'date(created_at)'). + where(conditions). + having(t[:project_id].in(Arel::Nodes::SqlLiteral.new(authed_projects.to_sql))) + end end end diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb index 4f81863da3..d76aa38f74 100644 --- a/lib/gitlab/data_builder/push.rb +++ b/lib/gitlab/data_builder/push.rb @@ -83,7 +83,7 @@ module Gitlab tag = repository.find_tag(tag_name) if tag - commit = repository.commit(tag.target) + commit = repository.commit(tag.dereferenced_target) commit.try(:sha) end else diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 799794c017..bcbf645599 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -2,8 +2,18 @@ # class return an instance of `GitlabAccessStatus` module Gitlab class GitAccess + UnauthorizedError = Class.new(StandardError) + + ERROR_MESSAGES = { + upload: 'You are not allowed to upload code for this project.', + download: 'You are not allowed to download code from this project.', + deploy_key: 'Deploy keys are not allowed to push code.', + no_repo: 'A repository for this project does not exist yet.' + } + DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive } PUSH_COMMANDS = %w{ git-receive-pack } + ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities @@ -16,56 +26,43 @@ module Gitlab end def check(cmd, changes) - return build_status_object(false, "Git access over #{protocol.upcase} is not allowed") unless protocol_allowed? - - unless actor - return build_status_object(false, "No user or key was provided.") - end - - if user && !user_access.allowed? - return build_status_object(false, "Your account has been blocked.") - end - - unless project && (user_access.can_read_project? || deploy_key_can_read_project?) - return build_status_object(false, 'The project you were looking for could not be found.') - end + check_protocol! + check_active_user! + check_project_accessibility! + check_command_existence!(cmd) case cmd when *DOWNLOAD_COMMANDS download_access_check when *PUSH_COMMANDS push_access_check(changes) - else - build_status_object(false, "The command you're trying to execute is not allowed.") end + + build_status_object(true) + rescue UnauthorizedError => ex + build_status_object(false, ex.message) end def download_access_check if user user_download_access_check - elsif deploy_key - build_status_object(true) - else - raise 'Wrong actor' + elsif deploy_key.nil? && !Guest.can?(:download_code, project) + raise UnauthorizedError, ERROR_MESSAGES[:download] end end def push_access_check(changes) if user user_push_access_check(changes) - elsif deploy_key - build_status_object(false, "Deploy keys are not allowed to push code.") else - raise 'Wrong actor' + raise UnauthorizedError, ERROR_MESSAGES[deploy_key ? :deploy_key : :upload] end end def user_download_access_check unless user_can_download_code? || build_can_download_code? - return build_status_object(false, "You are not allowed to download code from this project.") + raise UnauthorizedError, ERROR_MESSAGES[:download] end - - build_status_object(true) end def user_can_download_code? @@ -78,15 +75,15 @@ module Gitlab def user_push_access_check(changes) unless authentication_abilities.include?(:push_code) - return build_status_object(false, "You are not allowed to upload code for this project.") + raise UnauthorizedError, ERROR_MESSAGES[:upload] end if changes.blank? - return build_status_object(true) + return # Allow access. end unless project.repository.exists? - return build_status_object(false, "A repository for this project does not exist yet.") + raise UnauthorizedError, ERROR_MESSAGES[:no_repo] end changes_list = Gitlab::ChangesList.new(changes) @@ -96,11 +93,9 @@ module Gitlab status = change_access_check(change) unless status.allowed? # If user does not have access to make at least one change - cancel all push - return status + raise UnauthorizedError, status.message end end - - build_status_object(true) end def change_access_check(change) @@ -113,6 +108,30 @@ module Gitlab private + def check_protocol! + unless protocol_allowed? + raise UnauthorizedError, "Git access over #{protocol.upcase} is not allowed" + end + end + + def check_active_user! + if user && !user_access.allowed? + raise UnauthorizedError, "Your account has been blocked." + end + end + + def check_project_accessibility! + if project.blank? || !can_read_project? + raise UnauthorizedError, 'The project you were looking for could not be found.' + end + end + + def check_command_existence!(cmd) + unless ALL_COMMANDS.include?(cmd) + raise UnauthorizedError, "The command you're trying to execute is not allowed." + end + end + def matching_merge_request?(newrev, branch_name) Checks::MatchingMergeRequest.new(newrev, branch_name, project).match? end @@ -130,6 +149,16 @@ module Gitlab end end + def can_read_project? + if user + user_access.can_read_project? + elsif deploy_key + deploy_key_can_read_project? + else + Guest.can?(:read_project, project) + end + end + protected def user diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index 0a91d3918d..a8b4dc2a83 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -102,6 +102,8 @@ module Gitlab Gitlab::LDAP::Config.providers.each do |provider| adapter = Gitlab::LDAP::Adapter.new(provider) @ldap_person = Gitlab::LDAP::Person.find_by_uid(auth_hash.uid, adapter) + # The `uid` might actually be a DN. Try it next. + @ldap_person ||= Gitlab::LDAP::Person.find_by_dn(auth_hash.uid, adapter) break if @ldap_person end @ldap_person diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb index e59ead5d76..4c395b4266 100644 --- a/lib/gitlab/utils.rb +++ b/lib/gitlab/utils.rb @@ -13,5 +13,13 @@ module Gitlab def force_utf8(str) str.force_encoding(Encoding::UTF_8) end + + def to_boolean(value) + return value if [true, false].include?(value) + return true if value =~ /^(true|t|yes|y|1|on)$/i + return false if value =~ /^(false|f|no|n|0|off)$/i + + nil + end end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 940d54f868..029eff6605 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -39,6 +39,17 @@ describe Projects::MergeRequestsController do end end + shared_examples "loads labels" do |action| + it "loads labels into the @labels variable" do + get action, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: merge_request.iid, + format: 'html' + expect(assigns(:labels)).not_to be_nil + end + end + describe "GET show" do shared_examples "export merge as" do |format| it "does generally work" do @@ -51,6 +62,8 @@ describe Projects::MergeRequestsController do expect(response).to be_success end + it_behaves_like "loads labels", :show + it "generates it" do expect_any_instance_of(MergeRequest).to receive(:"to_#{format}") @@ -340,6 +353,8 @@ describe Projects::MergeRequestsController do get :diffs, params.merge(extra_params) end + it_behaves_like "loads labels", :diffs + context 'with default params' do context 'as html' do before { go(format: 'html') } @@ -546,6 +561,8 @@ describe Projects::MergeRequestsController do format: format end + it_behaves_like "loads labels", :commits + context 'as html' do it 'renders the show template' do go @@ -564,6 +581,14 @@ describe Projects::MergeRequestsController do end end + describe 'GET builds' do + it_behaves_like "loads labels", :builds + end + + describe 'GET pipelines' do + it_behaves_like "loads labels", :pipelines + end + describe 'GET conflicts' do let(:json_response) { JSON.parse(response.body) } diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 4065e2defb..2190d66488 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -49,13 +49,17 @@ FactoryGirl.define do end after(:create) do |project, evaluator| + # Builds and MRs can't have higher visibility level than repository access level. + builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min + merge_requests_access_level = [evaluator.merge_requests_access_level, evaluator.repository_access_level].min + project.project_feature. - update_attributes( + update_attributes!( wiki_access_level: evaluator.wiki_access_level, - builds_access_level: evaluator.builds_access_level, + builds_access_level: builds_access_level, snippets_access_level: evaluator.snippets_access_level, issues_access_level: evaluator.issues_access_level, - merge_requests_access_level: evaluator.merge_requests_access_level, + merge_requests_access_level: merge_requests_access_level, repository_access_level: evaluator.repository_access_level ) end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 5910803df5..cd53a485ef 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -12,11 +12,15 @@ describe 'Commits' do end let!(:pipeline) do - FactoryGirl.create :ci_pipeline, project: project, sha: project.commit.sha + create(:ci_pipeline, + project: project, + ref: project.default_branch, + sha: project.commit.sha, + status: :success) end context 'commit status is Generic Commit Status' do - let!(:status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline } + let!(:status) { create(:generic_commit_status, pipeline: pipeline) } before do project.team << [@user, :reporter] @@ -39,7 +43,7 @@ describe 'Commits' do end context 'commit status is Ci Build' do - let!(:build) { FactoryGirl.create :ci_build, pipeline: pipeline } + let!(:build) { create(:ci_build, pipeline: pipeline) } let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } context 'when logged as developer' do @@ -48,13 +52,22 @@ describe 'Commits' do end describe 'Project commits' do + let!(:pipeline_from_other_branch) do + create(:ci_pipeline, + project: project, + ref: 'fix', + sha: project.commit.sha, + status: :failed) + end + before do visit namespace_project_commits_path(project.namespace, project, :master) end - it 'shows build status' do + it 'shows correct build status from default branch' do page.within("//li[@id='commit-#{pipeline.short_sha}']") do - expect(page).to have_css(".ci-status-link") + expect(page).to have_css('.ci-status-link') + expect(page).to have_css('.ci-status-icon-success') end end end diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb new file mode 100644 index 0000000000..476eca17a9 --- /dev/null +++ b/spec/features/groups/issues_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' + +feature 'Group issues page', feature: true do + let(:path) { issues_group_path(group) } + let(:issuable) { create(:issue, project: project, title: "this is my created issuable")} + + include_examples 'project features apply to issuables', Issue +end diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb new file mode 100644 index 0000000000..a2791b5754 --- /dev/null +++ b/spec/features/groups/merge_requests_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' + +feature 'Group merge requests page', feature: true do + let(:path) { merge_requests_group_path(group) } + let(:issuable) { create(:merge_request, source_project: project, target_project: project, title: "this is my created issuable")} + + include_examples 'project features apply to issuables', MergeRequest +end diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb index 485dc56006..29b3e606c3 100644 --- a/spec/features/issues/filter_by_milestone_spec.rb +++ b/spec/features/issues/filter_by_milestone_spec.rb @@ -11,6 +11,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(Milestone::None.title) + expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'No Milestone') expect(page).to have_css('.issue', count: 1) end @@ -22,6 +23,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(Milestone::Upcoming.title) + expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') expect(page).to have_css('.issue', count: 0) end @@ -33,6 +35,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(Milestone::Upcoming.title) + expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') expect(page).to have_css('.issue', count: 1) end @@ -44,6 +47,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(Milestone::Upcoming.title) + expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') expect(page).to have_css('.issue', count: 0) end end @@ -55,6 +59,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(milestone.title) + expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title) expect(page).to have_css('.issue', count: 1) end diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb index fb0c470428..3e424b547c 100644 --- a/spec/features/issues/new_branch_button_spec.rb +++ b/spec/features/issues/new_branch_button_spec.rb @@ -22,6 +22,7 @@ feature 'Start new branch from an issue', feature: true do create(:note, :on_issue, :system, project: project, note: "Mentioned in !#{referenced_mr.iid}") end + let(:referenced_mr) do create(:merge_request, :simple, source_project: project, target_project: project, description: "Fixes ##{issue.iid}", author: user) diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb index c77e719c5d..c46bd8d449 100644 --- a/spec/features/merge_requests/edit_mr_spec.rb +++ b/spec/features/merge_requests/edit_mr_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' feature 'Edit Merge Request', feature: true do let(:user) { create(:user) } let(:project) { create(:project, :public) } - let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) } + let(:merge_request) { create(:merge_request, :simple, source_project: project) } before do project.team << [user, :master] @@ -28,5 +28,17 @@ feature 'Edit Merge Request', feature: true do expect(page).to have_content 'Someone edited the merge request the same time you did' end + + it 'allows to unselect "Remove source branch"' do + merge_request.update(merge_params: { 'force_remove_source_branch' => '1' }) + expect(merge_request.merge_params['force_remove_source_branch']).to be_truthy + + visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request) + uncheck 'Remove source branch when merge request is accepted' + + click_button 'Save changes' + + expect(page).to have_content 'Remove source branch' + end end end diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index 1d4484a9ed..d25cf7fb35 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -41,6 +41,22 @@ describe 'Edit Project Settings', feature: true do end end end + + context "pipelines subtabs" do + it "shows builds when enabled" do + visit namespace_project_pipelines_path(project.namespace, project) + + expect(page).to have_selector(".shortcuts-builds") + end + + it "hides builds when disabled" do + allow(Ability).to receive(:allowed?).with(member, :read_builds, project).and_return(false) + + visit namespace_project_pipelines_path(project.namespace, project) + + expect(page).not_to have_selector(".shortcuts-builds") + end + end end describe 'project features visibility pages' do diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb new file mode 100644 index 0000000000..abfc46601f --- /dev/null +++ b/spec/features/projects/new_project_spec.rb @@ -0,0 +1,19 @@ +require "spec_helper" + +feature "New project", feature: true do + context "Visibility level selector" do + let(:user) { create(:admin) } + + before { login_as(user) } + + Gitlab::VisibilityLevel.options.each do |key, level| + it "sets selector to #{key}" do + stub_application_setting(default_project_visibility: level) + + visit new_project_path + + expect(find_field("project_visibility_level_#{level}")).to be_checked + end + end + end +end diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb index b3ba40b35a..472491188c 100644 --- a/spec/features/projects/ref_switcher_spec.rb +++ b/spec/features/projects/ref_switcher_spec.rb @@ -22,8 +22,20 @@ feature 'Ref switcher', feature: true, js: true do input.native.send_keys :down input.native.send_keys :down input.native.send_keys :enter - - expect(page).to have_content 'expand-collapse-files' end + + expect(page).to have_title 'expand-collapse-files' + end + + it "user selects ref with special characters" do + click_button 'master' + wait_for_ajax + + page.within '.project-refs-form' do + page.fill_in 'Search branches and tags', with: "'test'" + click_link "'test'" + end + + expect(page).to have_title "'test'" end end diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb new file mode 100644 index 0000000000..b4f5f6b3fc --- /dev/null +++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe 'Projects > Wiki > User views wiki in project page', feature: true do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + + before do + project.team << [user, :master] + login_as(user) + end + + context 'when repository is disabled for project' do + before do + project.project_feature.update!( + repository_access_level: ProjectFeature::DISABLED, + merge_requests_access_level: ProjectFeature::DISABLED, + builds_access_level: ProjectFeature::DISABLED + ) + end + + context 'when wiki homepage contains a link' do + before do + WikiPages::CreateService.new( + project, + user, + title: 'home', + content: '[some link](other-page)' + ).execute + end + + it 'displays the correct URL for the link' do + visit namespace_project_path(project.namespace, project) + expect(page).to have_link( + 'some link', + href: namespace_project_wiki_path( + project.namespace, + project, + 'other-page' + ) + ) + end + end + end +end diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb index 6fce11de30..db60c01db0 100644 --- a/spec/finders/branches_finder_spec.rb +++ b/spec/finders/branches_finder_spec.rb @@ -21,7 +21,7 @@ describe BranchesFinder do result = branches_finder.execute recently_updated_branch = repository.branches.max do |a, b| - repository.commit(a.target).committed_date <=> repository.commit(b.target).committed_date + repository.commit(a.dereferenced_target).committed_date <=> repository.commit(b.dereferenced_target).committed_date end expect(result.first.name).to eq(recently_updated_branch.name) diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb index 10cfb66ec1..9085cc8deb 100644 --- a/spec/finders/labels_finder_spec.rb +++ b/spec/finders/labels_finder_spec.rb @@ -64,6 +64,21 @@ describe LabelsFinder do expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1] end + + context 'as an administrator' do + it 'does not return labels from another project' do + # Purposefully creating a project with _nothing_ associated to it + isolated_project = create(:empty_project) + admin = create(:admin) + + # project_3 has a label associated to it, which we don't want coming + # back when we ask for the isolated project's labels + project_3.team << [admin, :reporter] + finder = described_class.new(admin, project_id: isolated_project.id) + + expect(finder.execute).to be_empty + end + end end context 'filtering by title' do diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb index 2ac810e478..98b42e264d 100644 --- a/spec/finders/tags_finder_spec.rb +++ b/spec/finders/tags_finder_spec.rb @@ -20,7 +20,7 @@ describe TagsFinder do result = tags_finder.execute recently_updated_tag = repository.tags.max do |a, b| - repository.commit(a.target).committed_date <=> repository.commit(b.target).committed_date + repository.commit(a.dereferenced_target).committed_date <=> repository.commit(b.dereferenced_target).committed_date end expect(result.first.name).to eq(recently_updated_tag.name) diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb index dca7f99757..a6d2ea11fc 100644 --- a/spec/lib/banzai/filter/autolink_filter_spec.rb +++ b/spec/lib/banzai/filter/autolink_filter_spec.rb @@ -99,6 +99,28 @@ describe Banzai::Filter::AutolinkFilter, lib: true do expect(doc.at_css('a')['href']).to eq link end + it 'autolinks rdar' do + link = 'rdar://localhost.com/blah' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'does not autolink javascript' do + link = 'javascript://alert(document.cookie);' + doc = filter("See #{link}") + + expect(doc.at_css('a')).to be_nil + end + + it 'does not autolink bad URLs' do + link = 'foo://23423:::asdf' + doc = filter("See #{link}") + + expect(doc.to_s).to eq("See #{link}") + end + it 'does not include trailing punctuation' do doc = filter("See #{link}.") expect(doc.at_css('a').text).to eq link diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb index f181125156..0140a91c7b 100644 --- a/spec/lib/banzai/filter/redactor_filter_spec.rb +++ b/spec/lib/banzai/filter/redactor_filter_spec.rb @@ -28,31 +28,39 @@ describe Banzai::Filter::RedactorFilter, lib: true do and_return(parser_class) end - it 'removes unpermitted Project references' do - user = create(:user) - project = create(:empty_project) + context 'valid projects' do + before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(true) } - link = reference_link(project: project.id, reference_type: 'test') - doc = filter(link, current_user: user) + it 'allows permitted Project references' do + user = create(:user) + project = create(:empty_project) + project.team << [user, :master] - expect(doc.css('a').length).to eq 0 + link = reference_link(project: project.id, reference_type: 'test') + doc = filter(link, current_user: user) + + expect(doc.css('a').length).to eq 1 + end end - it 'allows permitted Project references' do - user = create(:user) - project = create(:empty_project) - project.team << [user, :master] + context 'invalid projects' do + before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(false) } - link = reference_link(project: project.id, reference_type: 'test') - doc = filter(link, current_user: user) + it 'removes unpermitted references' do + user = create(:user) + project = create(:empty_project) - expect(doc.css('a').length).to eq 1 - end + link = reference_link(project: project.id, reference_type: 'test') + doc = filter(link, current_user: user) - it 'handles invalid Project references' do - link = reference_link(project: 12345, reference_type: 'test') + expect(doc.css('a').length).to eq 0 + end - expect { filter(link) }.not_to raise_error + it 'handles invalid references' do + link = reference_link(project: 12345, reference_type: 'test') + + expect { filter(link) }.not_to raise_error + end end end diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb index 9095d2b134..aa127f0179 100644 --- a/spec/lib/banzai/reference_parser/base_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb @@ -27,41 +27,12 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do let(:link) { empty_html_link } context 'when the link has a data-project attribute' do - it 'returns the nodes if the attribute value equals the current project ID' do + it 'checks if user can read the resource' do link['data-project'] = project.id.to_s - expect(Ability).not_to receive(:allowed?) - expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) - end + expect(subject).to receive(:can_read_reference?).with(user, project) - it 'returns the nodes if the user can read the project' do - other_project = create(:empty_project, :public) - - link['data-project'] = other_project.id.to_s - - expect(Ability).to receive(:allowed?). - with(user, :read_project, other_project). - and_return(true) - - expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) - end - - it 'returns an empty Array when the attribute value is empty' do - link['data-project'] = '' - - expect(subject.nodes_visible_to_user(user, [link])).to eq([]) - end - - it 'returns an empty Array when the user can not read the project' do - other_project = create(:empty_project, :public) - - link['data-project'] = other_project.id.to_s - - expect(Ability).to receive(:allowed?). - with(user, :read_project, other_project). - and_return(false) - - expect(subject.nodes_visible_to_user(user, [link])).to eq([]) + subject.nodes_visible_to_user(user, [link]) end end diff --git a/spec/lib/banzai/reference_parser/commit_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_parser_spec.rb index 0b76d29fce..412ffa77c3 100644 --- a/spec/lib/banzai/reference_parser/commit_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb @@ -8,6 +8,14 @@ describe Banzai::ReferenceParser::CommitParser, lib: true do subject { described_class.new(project, user) } let(:link) { empty_html_link } + describe '#nodes_visible_to_user' do + context 'when the link has a data-issue attribute' do + before { link['data-commit'] = 123 } + + it_behaves_like "referenced feature visibility", "repository" + end + end + describe '#referenced_by' do context 'when the link has a data-project attribute' do before do diff --git a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb index ba982f3854..96e55b0997 100644 --- a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb @@ -8,6 +8,14 @@ describe Banzai::ReferenceParser::CommitRangeParser, lib: true do subject { described_class.new(project, user) } let(:link) { empty_html_link } + describe '#nodes_visible_to_user' do + context 'when the link has a data-issue attribute' do + before { link['data-commit-range'] = '123..456' } + + it_behaves_like "referenced feature visibility", "repository" + end + end + describe '#referenced_by' do context 'when the link has a data-project attribute' do before do diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb index a6ef8394fe..50a5d1a19b 100644 --- a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb @@ -8,6 +8,14 @@ describe Banzai::ReferenceParser::ExternalIssueParser, lib: true do subject { described_class.new(project, user) } let(:link) { empty_html_link } + describe '#nodes_visible_to_user' do + context 'when the link has a data-issue attribute' do + before { link['data-external-issue'] = 123 } + + it_behaves_like "referenced feature visibility", "issues" + end + end + describe '#referenced_by' do context 'when the link has a data-project attribute' do before do diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb index 85cfe728b6..6873b7b85f 100644 --- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb @@ -4,10 +4,10 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do include ReferenceParserHelpers let(:project) { create(:empty_project, :public) } - let(:user) { create(:user) } - let(:issue) { create(:issue, project: project) } - subject { described_class.new(project, user) } - let(:link) { empty_html_link } + let(:user) { create(:user) } + let(:issue) { create(:issue, project: project) } + let(:link) { empty_html_link } + subject { described_class.new(project, user) } describe '#nodes_visible_to_user' do context 'when the link has a data-issue attribute' do @@ -15,6 +15,8 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do link['data-issue'] = issue.id.to_s end + it_behaves_like "referenced feature visibility", "issues" + it 'returns the nodes when the user can read the issue' do expect(Ability).to receive(:issues_readable_by_user). with([issue], user). diff --git a/spec/lib/banzai/reference_parser/label_parser_spec.rb b/spec/lib/banzai/reference_parser/label_parser_spec.rb index 77fda47f0e..8c540d35dd 100644 --- a/spec/lib/banzai/reference_parser/label_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/label_parser_spec.rb @@ -9,6 +9,14 @@ describe Banzai::ReferenceParser::LabelParser, lib: true do subject { described_class.new(project, user) } let(:link) { empty_html_link } + describe '#nodes_visible_to_user' do + context 'when the link has a data-issue attribute' do + before { link['data-label'] = label.id.to_s } + + it_behaves_like "referenced feature visibility", "issues", "merge_requests" + end + end + describe '#referenced_by' do describe 'when the link has a data-label attribute' do context 'using an existing label ID' do diff --git a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb index cf89ad598e..cb69ca1680 100644 --- a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb @@ -8,6 +8,19 @@ describe Banzai::ReferenceParser::MergeRequestParser, lib: true do subject { described_class.new(merge_request.target_project, user) } let(:link) { empty_html_link } + describe '#nodes_visible_to_user' do + context 'when the link has a data-issue attribute' do + let(:project) { merge_request.target_project } + + before do + project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) + link['data-merge-request'] = merge_request.id.to_s + end + + it_behaves_like "referenced feature visibility", "merge_requests" + end + end + describe '#referenced_by' do describe 'when the link has a data-merge-request attribute' do context 'using an existing merge request ID' do diff --git a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb index 6aa45a22cc..2d4d589ae3 100644 --- a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb @@ -9,6 +9,14 @@ describe Banzai::ReferenceParser::MilestoneParser, lib: true do subject { described_class.new(project, user) } let(:link) { empty_html_link } + describe '#nodes_visible_to_user' do + context 'when the link has a data-issue attribute' do + before { link['data-milestone'] = milestone.id.to_s } + + it_behaves_like "referenced feature visibility", "issues", "merge_requests" + end + end + describe '#referenced_by' do describe 'when the link has a data-milestone attribute' do context 'using an existing milestone ID' do diff --git a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb index 59127b7c5d..d217a77580 100644 --- a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb @@ -9,6 +9,14 @@ describe Banzai::ReferenceParser::SnippetParser, lib: true do subject { described_class.new(project, user) } let(:link) { empty_html_link } + describe '#nodes_visible_to_user' do + context 'when the link has a data-issue attribute' do + before { link['data-snippet'] = snippet.id.to_s } + + it_behaves_like "referenced feature visibility", "snippets" + end + end + describe '#referenced_by' do describe 'when the link has a data-snippet attribute' do context 'using an existing snippet ID' do diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb index 4e7f82a6e0..fafc2cec54 100644 --- a/spec/lib/banzai/reference_parser/user_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb @@ -103,6 +103,8 @@ describe Banzai::ReferenceParser::UserParser, lib: true do it 'returns the nodes if the attribute value equals the current project ID' do link['data-project'] = project.id.to_s + # Ensure that we dont call for Ability.allowed? + # When project_id in the node is equal to current project ID expect(Ability).not_to receive(:allowed?) expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index de3f64249a..1bbaca0739 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -257,8 +257,9 @@ describe Gitlab::ClosingIssueExtractor, lib: true do context 'with an external issue tracker reference' do it 'extracts the referenced issue' do jira_project = create(:jira_project, name: 'JIRA_EXT1') + jira_project.team << [jira_project.creator, :master] jira_issue = ExternalIssue.new("#{jira_project.name}-1", project: jira_project) - closing_issue_extractor = described_class.new jira_project + closing_issue_extractor = described_class.new(jira_project, jira_project.creator) message = "Resolve #{jira_issue.to_reference}" expect(closing_issue_extractor.closed_by_message(message)).to eq([jira_issue]) diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb new file mode 100644 index 0000000000..01b2a55b63 --- /dev/null +++ b/spec/lib/gitlab/contributions_calendar_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe Gitlab::ContributionsCalendar do + let(:contributor) { create(:user) } + let(:user) { create(:user) } + + let(:private_project) do + create(:empty_project, :private) do |project| + create(:project_member, user: contributor, project: project) + end + end + + let(:public_project) do + create(:empty_project, :public) do |project| + create(:project_member, user: contributor, project: project) + end + end + + let(:feature_project) do + create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) do |project| + create(:project_member, user: contributor, project: project).project + end + end + + let(:today) { Time.now.to_date } + let(:last_week) { today - 7.days } + let(:last_year) { today - 1.year } + + before do + travel_to today + end + + after do + travel_back + end + + def calendar(current_user = nil) + described_class.new(contributor, current_user) + end + + def create_event(project, day) + @targets ||= {} + @targets[project] ||= create(:issue, project: project, author: contributor) + + Event.create!( + project: project, + action: Event::CREATED, + target: @targets[project], + author: contributor, + created_at: day, + ) + end + + describe '#activity_dates' do + it "returns a hash of date => count" do + create_event(public_project, last_week) + create_event(public_project, last_week) + create_event(public_project, today) + + expect(calendar.activity_dates).to eq(last_week => 2, today => 1) + end + + it "only shows private events to authorized users" do + create_event(private_project, today) + create_event(feature_project, today) + + expect(calendar.activity_dates[today]).to eq(0) + expect(calendar(user).activity_dates[today]).to eq(0) + expect(calendar(contributor).activity_dates[today]).to eq(2) + end + end + + describe '#events_by_date' do + it "returns all events for a given date" do + e1 = create_event(public_project, today) + e2 = create_event(public_project, today) + create_event(public_project, last_week) + + expect(calendar.events_by_date(today)).to contain_exactly(e1, e2) + end + + it "only shows private events to authorized users" do + e1 = create_event(public_project, today) + e2 = create_event(private_project, today) + e3 = create_event(feature_project, today) + create_event(public_project, last_week) + + expect(calendar.events_by_date(today)).to contain_exactly(e1) + expect(calendar(contributor).events_by_date(today)).to contain_exactly(e1, e2, e3) + end + end + + describe '#starting_year' do + it "should be the start of last year" do + expect(calendar.starting_year).to eq(last_year.year) + end + end + + describe '#starting_month' do + it "should be the start of this month" do + expect(calendar.starting_month).to eq(today.month) + end + end +end diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb index f045463c1c..6b3dfebd85 100644 --- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Gfm::ReferenceRewriter do let(:new_project) { create(:project, name: 'new') } let(:user) { create(:user) } - before { old_project.team << [user, :guest] } + before { old_project.team << [user, :reporter] } describe '#rewrite' do subject do diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index a5aa387f4f..f1d0a19000 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -66,6 +66,7 @@ describe Gitlab::GitAccess, lib: true do context 'pull code' do it { expect(subject.allowed?).to be_falsey } + it { expect(subject.message).to match(/You are not allowed to download code/) } end end @@ -77,6 +78,7 @@ describe Gitlab::GitAccess, lib: true do context 'pull code' do it { expect(subject.allowed?).to be_falsey } + it { expect(subject.message).to match(/Your account has been blocked/) } end end @@ -84,6 +86,29 @@ describe Gitlab::GitAccess, lib: true do context 'pull code' do it { expect(subject.allowed?).to be_falsey } end + + context 'when project is public' do + let(:public_project) { create(:project, :public) } + let(:guest_access) { Gitlab::GitAccess.new(nil, public_project, 'web', authentication_abilities: []) } + subject { guest_access.check('git-upload-pack', '_any') } + + context 'when repository is enabled' do + it 'give access to download code' do + public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::ENABLED) + + expect(subject.allowed?).to be_truthy + end + end + + context 'when repository is disabled' do + it 'does not give access to download code' do + public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED) + + expect(subject.allowed?).to be_falsey + expect(subject.message).to match(/You are not allowed to download code/) + end + end + end end describe 'deploy key permissions' do @@ -122,6 +147,14 @@ describe Gitlab::GitAccess, lib: true do describe 'build authentication_abilities permissions' do let(:authentication_abilities) { build_authentication_abilities } + describe 'owner' do + let(:project) { create(:project, namespace: user.namespace) } + + context 'pull code' do + it { expect(subject).to be_allowed } + end + end + describe 'reporter user' do before { project.team << [user, :reporter] } diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 576cda595b..576aa5c366 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -18,7 +18,7 @@ describe Gitlab::GitAccessWiki, lib: true do project.team << [user, :developer] end - subject { access.push_access_check(changes) } + subject { access.check('git-receive-pack', changes) } it { expect(subject.allowed?).to be_truthy } end diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 78c669e8fa..fc9e1cb430 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -137,11 +137,12 @@ describe Gitlab::OAuth::User, lib: true do allow(ldap_user).to receive(:username) { uid } allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] } allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } - allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) end context "and no account for the LDAP user" do it "creates a user with dual LDAP and omniauth identities" do + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) + oauth_user.save expect(gl_user).to be_valid @@ -159,6 +160,8 @@ describe Gitlab::OAuth::User, lib: true do context "and LDAP user has an account already" do let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } it "adds the omniauth identity to the LDAP account" do + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) + oauth_user.save expect(gl_user).to be_valid @@ -172,6 +175,24 @@ describe Gitlab::OAuth::User, lib: true do ]) end end + + context 'when an LDAP person is not found by uid' do + it 'tries to find an LDAP person by DN and adds the omniauth identity to the user' do + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) + allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user) + + oauth_user.save + + identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } + expect(identities_as_hash) + .to match_array( + [ + { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + { provider: 'twitter', extern_uid: uid } + ] + ) + end + end end context "and no corresponding LDAP person" do diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 7b4ccc8391..bf0ab9635f 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe Gitlab::ReferenceExtractor, lib: true do let(:project) { create(:project) } + before { project.team << [project.creator, :developer] } + subject { Gitlab::ReferenceExtractor.new(project, project.creator) } it 'accesses valid user objects' do @@ -42,7 +44,6 @@ describe Gitlab::ReferenceExtractor, lib: true do end it 'accesses valid issue objects' do - project.team << [project.creator, :developer] @i0 = create(:issue, project: project) @i1 = create(:issue, project: project) diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb new file mode 100644 index 0000000000..d5d8731087 --- /dev/null +++ b/spec/lib/gitlab/utils_spec.rb @@ -0,0 +1,35 @@ +describe Gitlab::Utils, lib: true do + def to_boolean(value) + described_class.to_boolean(value) + end + + describe '.to_boolean' do + it 'accepts booleans' do + expect(to_boolean(true)).to be(true) + expect(to_boolean(false)).to be(false) + end + + it 'converts a valid string to a boolean' do + expect(to_boolean(true)).to be(true) + expect(to_boolean('true')).to be(true) + expect(to_boolean('YeS')).to be(true) + expect(to_boolean('t')).to be(true) + expect(to_boolean('1')).to be(true) + expect(to_boolean('ON')).to be(true) + + expect(to_boolean('FaLse')).to be(false) + expect(to_boolean('F')).to be(false) + expect(to_boolean('NO')).to be(false) + expect(to_boolean('n')).to be(false) + expect(to_boolean('0')).to be(false) + expect(to_boolean('oFF')).to be(false) + end + + it 'converts an invalid string to nil' do + expect(to_boolean('fals')).to be_nil + expect(to_boolean('yeah')).to be_nil + expect(to_boolean('')).to be_nil + expect(to_boolean(nil)).to be_nil + end + end +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index cc215d252f..2b76e056f3 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -41,14 +41,62 @@ describe ApplicationSetting, models: true do subject { setting } end - context 'repository storages inclussion' do + # Upgraded databases will have this sort of content + context 'repository_storages is a String, not an Array' do + before { setting.__send__(:raw_write_attribute, :repository_storages, 'default') } + + it { expect(setting.repository_storages_before_type_cast).to eq('default') } + it { expect(setting.repository_storages).to eq(['default']) } + end + + context 'repository storages' do before do - storages = { 'custom' => 'tmp/tests/custom_repositories' } + storages = { + 'custom1' => 'tmp/tests/custom_repositories_1', + 'custom2' => 'tmp/tests/custom_repositories_2', + 'custom3' => 'tmp/tests/custom_repositories_3', + + } allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) end - it { is_expected.to allow_value('custom').for(:repository_storage) } - it { is_expected.not_to allow_value('alternative').for(:repository_storage) } + describe 'inclusion' do + it { is_expected.to allow_value('custom1').for(:repository_storages) } + it { is_expected.to allow_value(['custom2', 'custom3']).for(:repository_storages) } + it { is_expected.not_to allow_value('alternative').for(:repository_storages) } + it { is_expected.not_to allow_value(['alternative', 'custom1']).for(:repository_storages) } + end + + describe 'presence' do + it { is_expected.not_to allow_value([]).for(:repository_storages) } + it { is_expected.not_to allow_value("").for(:repository_storages) } + it { is_expected.not_to allow_value(nil).for(:repository_storages) } + end + + describe '.pick_repository_storage' do + it 'uses Array#sample to pick a random storage' do + array = double('array', sample: 'random') + expect(setting).to receive(:repository_storages).and_return(array) + + expect(setting.pick_repository_storage).to eq('random') + end + + describe '#repository_storage' do + it 'returns the first storage' do + setting.repository_storages = ['good', 'bad'] + + expect(setting.repository_storage).to eq('good') + end + end + + describe '#repository_storage=' do + it 'overwrites repository_storages' do + setting.repository_storage = 'overwritten' + + expect(setting.repository_storages).to eq(['overwritten']) + end + end + end end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 51be3f3613..e3bb3482d6 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -205,12 +205,53 @@ eos end end - describe '#ci_commits' do - # TODO: kamil - end - describe '#status' do - # TODO: kamil + context 'without arguments for compound status' do + shared_examples 'giving the status from pipeline' do + it do + expect(commit.status).to eq(Ci::Pipeline.status) + end + end + + context 'with pipelines' do + let!(:pipeline) do + create(:ci_empty_pipeline, project: project, sha: commit.sha) + end + + it_behaves_like 'giving the status from pipeline' + end + + context 'without pipelines' do + it_behaves_like 'giving the status from pipeline' + end + end + + context 'when a particular ref is specified' do + let!(:pipeline_from_master) do + create(:ci_empty_pipeline, + project: project, + sha: commit.sha, + ref: 'master', + status: 'failed') + end + + let!(:pipeline_from_fix) do + create(:ci_empty_pipeline, + project: project, + sha: commit.sha, + ref: 'fix', + status: 'success') + end + + it 'gives pipelines from a particular branch' do + expect(commit.status('master')).to eq(pipeline_from_master.status) + expect(commit.status('fix')).to eq(pipeline_from_fix.status) + end + + it 'gives compound status if ref is nil' do + expect(commit.status(nil)).to eq(commit.status) + end + end end describe '#participants' do diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index a59d30687f..4606bc24b3 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -97,6 +97,11 @@ describe Issue, "Issuable" do end end + describe '.to_ability_name' do + it { expect(Issue.to_ability_name).to eq("issue") } + it { expect(MergeRequest.to_ability_name).to eq("merge_request") } + end + describe "#today?" do it "returns true when created today" do # Avoid timezone differences and just return exactly what we want diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb index 5363aea4d2..9041690023 100644 --- a/spec/models/concerns/project_features_compatibility_spec.rb +++ b/spec/models/concerns/project_features_compatibility_spec.rb @@ -22,4 +22,18 @@ describe ProjectFeaturesCompatibility do expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED) end end + + it "converts fields from true to ProjectFeature::ENABLED" do + features.each do |feature| + project.update_attribute("#{feature}_enabled".to_sym, true) + expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::ENABLED) + end + end + + it "converts fields from false to ProjectFeature::DISABLED" do + features.each do |feature| + project.update_attribute("#{feature}_enabled".to_sym, false) + expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED) + end + end end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 733b79079e..ebe46efc5a 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -27,13 +27,14 @@ describe Event, models: true do end describe "Push event" do - let(:project) { create(:project) } + let(:project) { create(:project, :private) } let(:user) { project.owner } let(:event) { create_event(project, user) } it do expect(event.push?).to be_truthy - expect(event.visible_to_user?).to be_truthy + expect(event.visible_to_user?(user)).to be_truthy + expect(event.visible_to_user?(nil)).to be_falsey expect(event.tag?).to be_falsey expect(event.branch_name).to eq("master") expect(event.author).to eq(user) @@ -66,6 +67,7 @@ describe Event, models: true do let(:admin) { create(:admin) } let(:issue) { create(:issue, project: project, author: author, assignee: assignee) } let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) } + let(:note_on_commit) { create(:note_on_commit, project: project) } let(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) } let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project) } let(:event) { Event.new(project: project, target: target, author_id: author.id) } @@ -75,6 +77,32 @@ describe Event, models: true do project.team << [guest, :guest] end + context 'commit note event' do + let(:target) { note_on_commit } + + it do + aggregate_failures do + expect(event.visible_to_user?(non_member)).to eq true + expect(event.visible_to_user?(member)).to eq true + expect(event.visible_to_user?(guest)).to eq true + expect(event.visible_to_user?(admin)).to eq true + end + end + + context 'private project' do + let(:project) { create(:empty_project, :private) } + + it do + aggregate_failures do + expect(event.visible_to_user?(non_member)).to eq false + expect(event.visible_to_user?(member)).to eq true + expect(event.visible_to_user?(guest)).to eq false + expect(event.visible_to_user?(admin)).to eq true + end + end + end + end + context 'issue event' do context 'for non confidential issues' do let(:target) { issue } diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb new file mode 100644 index 0000000000..d79f929f7a --- /dev/null +++ b/spec/models/guest_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Guest, lib: true do + let(:public_project) { create(:project, :public) } + let(:private_project) { create(:project, :private) } + let(:internal_project) { create(:project, :internal) } + + describe '.can_pull?' do + context 'when project is private' do + it 'does not allow to pull the repo' do + expect(Guest.can?(:download_code, private_project)).to eq(false) + end + end + + context 'when project is internal' do + it 'does not allow to pull the repo' do + expect(Guest.can?(:download_code, internal_project)).to eq(false) + end + end + + context 'when project is public' do + context 'when repository is disabled' do + it 'does not allow to pull the repo' do + public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED) + + expect(Guest.can?(:download_code, public_project)).to eq(false) + end + end + + context 'when repository is accessible only by team members' do + it 'does not allow to pull the repo' do + public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::PRIVATE) + + expect(Guest.can?(:download_code, public_project)).to eq(false) + end + end + + context 'when repository is enabled' do + it 'allows to pull the repo' do + public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::ENABLED) + + expect(Guest.can?(:download_code, public_project)).to eq(true) + end + end + end + end +end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 3b8b743af2..75d104584f 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -22,7 +22,7 @@ describe Issue, models: true do it { is_expected.to have_db_index(:deleted_at) } end - describe 'visible_to_user' do + describe '.visible_to_user' do let(:user) { create(:user) } let(:authorized_user) { create(:user) } let(:project) { create(:project, namespace: authorized_user.namespace) } @@ -102,11 +102,17 @@ describe Issue, models: true do it 'returns the merge request to close this issue' do allow(mr).to receive(:closes_issue?).with(issue).and_return(true) - expect(issue.closed_by_merge_requests).to eq([mr]) + expect(issue.closed_by_merge_requests(mr.author)).to eq([mr]) + end + + it "returns an empty array when the merge request is closed already" do + closed_mr + + expect(issue.closed_by_merge_requests(closed_mr.author)).to eq([]) end it "returns an empty array when the current issue is closed already" do - expect(closed_issue.closed_by_merge_requests).to eq([]) + expect(closed_issue.closed_by_merge_requests(closed_issue.author)).to eq([]) end end @@ -212,7 +218,7 @@ describe Issue, models: true do source_project: subject.project, source_branch: "#{subject.iid}-branch" }) merge_request.create_cross_references!(user) - expect(subject.referenced_merge_requests).not_to be_empty + expect(subject.referenced_merge_requests(user)).not_to be_empty expect(subject.related_branches(user)).to eq([subject.to_branch_name]) end @@ -308,23 +314,6 @@ describe Issue, models: true do end describe '#visible_to_user?' do - context 'with a user' do - let(:user) { build(:user) } - let(:issue) { build(:issue) } - - it 'returns true when the issue is readable' do - expect(issue).to receive(:readable_by?).with(user).and_return(true) - - expect(issue.visible_to_user?(user)).to eq(true) - end - - it 'returns false when the issue is not readable' do - expect(issue).to receive(:readable_by?).with(user).and_return(false) - - expect(issue.visible_to_user?(user)).to eq(false) - end - end - context 'without a user' do let(:issue) { build(:issue) } @@ -340,9 +329,40 @@ describe Issue, models: true do expect(issue.visible_to_user?).to eq(false) end end - end - describe '#readable_by?' do + context 'with a user' do + let(:user) { build(:user) } + let(:issue) { build(:issue) } + + it 'returns true when the issue is readable' do + expect(issue).to receive(:readable_by?).with(user).and_return(true) + + expect(issue.visible_to_user?(user)).to eq(true) + end + + it 'returns false when the issue is not readable' do + expect(issue).to receive(:readable_by?).with(user).and_return(false) + + expect(issue.visible_to_user?(user)).to eq(false) + end + + it 'returns false when feature is disabled' do + expect(issue).not_to receive(:readable_by?) + + issue.project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) + + expect(issue.visible_to_user?(user)).to eq(false) + end + + it 'returns false when restricted for members' do + expect(issue).not_to receive(:readable_by?) + + issue.project.project_feature.update_attribute(:issues_access_level, ProjectFeature::PRIVATE) + + expect(issue.visible_to_user?(user)).to eq(false) + end + end + describe 'with a regular user that is not a team member' do let(:user) { create(:user) } @@ -352,13 +372,13 @@ describe Issue, models: true do it 'returns true for a regular issue' do issue = build(:issue, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end it 'returns false for a confidential issue' do issue = build(:issue, project: project, confidential: true) - expect(issue).not_to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(false) end end @@ -369,13 +389,13 @@ describe Issue, models: true do it 'returns true for a regular issue' do issue = build(:issue, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end it 'returns false for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).not_to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(false) end end @@ -387,13 +407,13 @@ describe Issue, models: true do it 'returns false for a regular issue' do issue = build(:issue, project: project) - expect(issue).not_to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(false) end it 'returns false for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).not_to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(false) end end end @@ -404,26 +424,28 @@ describe Issue, models: true do it 'returns false for a regular issue' do issue = build(:issue, project: project) - expect(issue).not_to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(false) end it 'returns false for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).not_to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(false) end context 'when the user is the project owner' do + before { project.team << [user, :master] } + it 'returns true for a regular issue' do issue = build(:issue, project: project) - expect(issue).not_to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end it 'returns true for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).not_to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end end end @@ -441,13 +463,13 @@ describe Issue, models: true do it 'returns true for a regular issue' do issue = build(:issue, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end it 'returns true for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end end @@ -461,13 +483,13 @@ describe Issue, models: true do it 'returns true for a regular issue' do issue = build(:issue, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end it 'returns true for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end end @@ -481,13 +503,13 @@ describe Issue, models: true do it 'returns true for a regular issue' do issue = build(:issue, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end it 'returns true for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end end end @@ -499,13 +521,13 @@ describe Issue, models: true do it 'returns true for a regular issue' do issue = build(:issue, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end it 'returns true for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end end end @@ -517,13 +539,13 @@ describe Issue, models: true do it 'returns true for a regular issue' do issue = build(:issue, project: project) - expect(issue).to be_publicly_visible + expect(issue).to be_truthy end it 'returns false for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).not_to be_publicly_visible + expect(issue).not_to be_falsy end end @@ -533,13 +555,13 @@ describe Issue, models: true do it 'returns false for a regular issue' do issue = build(:issue, project: project) - expect(issue).not_to be_publicly_visible + expect(issue).not_to be_falsy end it 'returns false for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).not_to be_publicly_visible + expect(issue).not_to be_falsy end end @@ -549,13 +571,13 @@ describe Issue, models: true do it 'returns false for a regular issue' do issue = build(:issue, project: project) - expect(issue).not_to be_publicly_visible + expect(issue).not_to be_falsy end it 'returns false for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).not_to be_publicly_visible + expect(issue).not_to be_falsy end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f4dda1ee55..0245897938 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -67,11 +67,11 @@ describe Project, models: true do it { is_expected.to have_many(:notification_settings).dependent(:destroy) } it { is_expected.to have_many(:forks).through(:forked_project_links) } - context 'after create' do - it "creates project feature" do + context 'after initialized' do + it "has a project_feature" do project = FactoryGirl.build(:project) - expect { project.save }.to change{ project.project_feature.present? }.from(false).to(true) + expect(project.project_feature.present?).to be_present end end @@ -837,16 +837,19 @@ describe Project, models: true do context 'repository storage by default' do let(:project) { create(:empty_project) } - subject { project.repository_storage } - before do - storages = { 'alternative_storage' => '/some/path' } + storages = { + 'default' => 'tmp/tests/repositories', + 'picked' => 'tmp/tests/repositories', + } allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) - stub_application_setting(repository_storage: 'alternative_storage') - allow_any_instance_of(Project).to receive(:ensure_dir_exist).and_return(true) end - it { is_expected.to eq('alternative_storage') } + it 'picks storage from ApplicationSetting' do + expect_any_instance_of(ApplicationSetting).to receive(:pick_repository_storage).and_return('picked') + + expect(project.repository_storage).to eq('picked') + end end context 'shared runners by default' do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 187a1bf2d7..19b3e7e470 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -68,8 +68,8 @@ describe Repository, models: true do double_first = double(committed_date: Time.now) double_last = double(committed_date: Time.now - 1.second) - allow(tag_a).to receive(:target).and_return(double_first) - allow(tag_b).to receive(:target).and_return(double_last) + allow(tag_a).to receive(:dereferenced_target).and_return(double_first) + allow(tag_b).to receive(:dereferenced_target).and_return(double_last) allow(repository).to receive(:tags).and_return([tag_a, tag_b]) end @@ -83,8 +83,8 @@ describe Repository, models: true do double_first = double(committed_date: Time.now - 1.second) double_last = double(committed_date: Time.now) - allow(tag_a).to receive(:target).and_return(double_last) - allow(tag_b).to receive(:target).and_return(double_first) + allow(tag_a).to receive(:dereferenced_target).and_return(double_last) + allow(tag_b).to receive(:dereferenced_target).and_return(double_first) allow(repository).to receive(:tags).and_return([tag_a, tag_b]) end @@ -632,9 +632,9 @@ describe Repository, models: true do context "when the branch wasn't empty" do it 'updates the head' do - expect(repository.find_branch('feature').target.id).to eq(old_rev) + expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev) repository.update_branch_with_hooks(user, 'feature') { new_rev } - expect(repository.find_branch('feature').target.id).to eq(new_rev) + expect(repository.find_branch('feature').dereferenced_target.id).to eq(new_rev) end end end @@ -659,7 +659,7 @@ describe Repository, models: true do context 'when the update would remove commits from the target branch' do it 'raises an exception' do branch = 'master' - old_rev = repository.find_branch(branch).target.sha + old_rev = repository.find_branch(branch).dereferenced_target.sha # The 'master' branch is NOT an ancestor of new_rev. expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev) diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 658e3c13a7..96249a7d8c 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -6,6 +6,7 @@ describe ProjectPolicy, models: true do let(:dev) { create(:user) } let(:master) { create(:user) } let(:owner) { create(:user) } + let(:admin) { create(:admin) } let(:project) { create(:empty_project, :public, namespace: owner.namespace) } let(:guest_permissions) do @@ -152,6 +153,19 @@ describe ProjectPolicy, models: true do context 'owner' do let(:current_user) { owner } + it do + is_expected.to include(*guest_permissions) + is_expected.to include(*reporter_permissions) + is_expected.to include(*team_member_reporter_permissions) + is_expected.to include(*developer_permissions) + is_expected.to include(*master_permissions) + is_expected.to include(*owner_permissions) + end + end + + context 'admin' do + let(:current_user) { admin } + it do is_expected.to include(*guest_permissions) is_expected.to include(*reporter_permissions) diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb index f7fe4c1087..01bb9e955e 100644 --- a/spec/requests/api/api_helpers_spec.rb +++ b/spec/requests/api/api_helpers_spec.rb @@ -265,36 +265,6 @@ describe API::Helpers, api: true do end end - describe '.to_boolean' do - it 'accepts booleans' do - expect(to_boolean(true)).to be(true) - expect(to_boolean(false)).to be(false) - end - - it 'converts a valid string to a boolean' do - expect(to_boolean(true)).to be(true) - expect(to_boolean('true')).to be(true) - expect(to_boolean('YeS')).to be(true) - expect(to_boolean('t')).to be(true) - expect(to_boolean('1')).to be(true) - expect(to_boolean('ON')).to be(true) - - expect(to_boolean('FaLse')).to be(false) - expect(to_boolean('F')).to be(false) - expect(to_boolean('NO')).to be(false) - expect(to_boolean('n')).to be(false) - expect(to_boolean('0')).to be(false) - expect(to_boolean('oFF')).to be(false) - end - - it 'converts an invalid string to nil' do - expect(to_boolean('fals')).to be_nil - expect(to_boolean('yeah')).to be_nil - expect(to_boolean('')).to be_nil - expect(to_boolean(nil)).to be_nil - end - end - describe '.handle_api_exception' do before do allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true) diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 867bc615b9..5c0f5eabeb 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -82,7 +82,20 @@ describe API::API, api: true do expect(json_response['message']['title']).to eq(['is invalid']) end - it 'returns 409 if label already exists' do + it 'returns 409 if label already exists in group' do + group = create(:group) + group_label = create(:group_label, group: group) + project.update(group: group) + + post api("/projects/#{project.id}/labels", user), + name: group_label.name, + color: '#FFAABB' + + expect(response).to have_http_status(409) + expect(json_response['message']).to eq('Label already exists') + end + + it 'returns 409 if label already exists in project' do post api("/projects/#{project.id}/labels", user), name: 'label1', color: '#FFAABB' diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index f4903d8e0b..096a8ebab7 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -33,6 +33,7 @@ describe API::API, 'Settings', api: true do expect(json_response['default_projects_limit']).to eq(3) expect(json_response['signin_enabled']).to be_falsey expect(json_response['repository_storage']).to eq('custom') + expect(json_response['repository_storages']).to eq(['custom']) expect(json_response['koding_enabled']).to be_truthy expect(json_response['koding_url']).to eq('http://koding.example.com') end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 27f0fd22ae..f1728d61de 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -115,6 +115,38 @@ describe 'Git HTTP requests', lib: true do end.to raise_error(JWT::DecodeError) end end + + context 'when the repo is public' do + context 'but the repo is disabled' do + it 'does not allow to clone the repo' do + project = create(:project, :public, repository_access_level: ProjectFeature::DISABLED) + + download("#{project.path_with_namespace}.git", {}) do |response| + expect(response).to have_http_status(:unauthorized) + end + end + end + + context 'but the repo is enabled' do + it 'allows to clone the repo' do + project = create(:project, :public, repository_access_level: ProjectFeature::ENABLED) + + download("#{project.path_with_namespace}.git", {}) do |response| + expect(response).to have_http_status(:ok) + end + end + end + + context 'but only project members are allowed' do + it 'does not allow to clone the repo' do + project = create(:project, :public, repository_access_level: ProjectFeature::PRIVATE) + + download("#{project.path_with_namespace}.git", {}) do |response| + expect(response).to have_http_status(:unauthorized) + end + end + end + end end context "when the project is private" do diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index f0ef155bd7..a3e7844b2f 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -20,7 +20,7 @@ describe JwtController do end end - context 'when using authorized request' do + context 'when using authenticated request' do context 'using CI token' do let(:build) { create(:ci_build, :running) } let(:project) { build.project } @@ -65,7 +65,7 @@ describe JwtController do let(:access_token) { create(:personal_access_token, user: user) } let(:headers) { { authorization: credentials(user.username, access_token.token) } } - it 'rejects the authorization attempt' do + it 'accepts the authorization attempt' do expect(response).to have_http_status(200) end end @@ -81,6 +81,20 @@ describe JwtController do end end + context 'when using unauthenticated request' do + it 'accepts the authorization attempt' do + get '/jwt/auth', parameters + + expect(response).to have_http_status(200) + end + + it 'allows read access' do + expect(service).to receive(:execute).with(authentication_abilities: Gitlab::Auth.read_authentication_abilities) + + get '/jwt/auth', parameters + end + end + context 'unknown service' do subject! { get '/jwt/auth', service: 'unknown' } diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index dbdf83a0df..9bfc84c742 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -284,7 +284,17 @@ describe 'Git LFS API and storage' do let(:authorization) { authorize_ci_project } shared_examples 'can download LFS only from own projects' do - context 'for own project' do + context 'for owned project' do + let(:project) { create(:empty_project, namespace: user.namespace) } + + let(:update_permissions) do + project.lfs_objects << lfs_object + end + + it_behaves_like 'responds with a file' + end + + context 'for member of project' do let(:pipeline) { create(:ci_empty_pipeline, project: project) } let(:update_permissions) do diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index c64df4979b..bb26513103 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -245,6 +245,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do it_behaves_like 'a pullable' end + + context 'when you are owner' do + let(:project) { create(:empty_project, namespace: current_user.namespace) } + + it_behaves_like 'a pullable' + end end context 'for private' do @@ -266,6 +272,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do it_behaves_like 'a pullable' end + + context 'when you are owner' do + let(:project) { create(:empty_project, namespace: current_user.namespace) } + + it_behaves_like 'a pullable' + end end end end @@ -276,13 +288,21 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'disallow for all' do - let(:project) { create(:empty_project, :public) } + context 'when you are member' do + let(:project) { create(:empty_project, :public) } - before do - project.team << [current_user, :developer] + before do + project.team << [current_user, :developer] + end + + it_behaves_like 'an inaccessible' end - it_behaves_like 'an inaccessible' + context 'when you are owner' do + let(:project) { create(:empty_project, :public, namespace: current_user.namespace) } + + it_behaves_like 'an inaccessible' + end end end end diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb index a4fcd44882..0879e3ab4c 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git_tag_push_service_spec.rb @@ -37,65 +37,138 @@ describe GitTagPushService, services: true do end describe "Git Tag Push Data" do - before do - service.execute - @push_data = service.push_data - @tag_name = Gitlab::Git.ref_name(ref) - @tag = project.repository.find_tag(@tag_name) - @commit = project.commit(@tag.target) - end - subject { @push_data } + let(:tag) { project.repository.find_tag(tag_name) } + let(:commit) { tag.dereferenced_target } - it { is_expected.to include(object_kind: 'tag_push') } - it { is_expected.to include(ref: ref) } - it { is_expected.to include(before: oldrev) } - it { is_expected.to include(after: newrev) } - it { is_expected.to include(message: @tag.message) } - it { is_expected.to include(user_id: user.id) } - it { is_expected.to include(user_name: user.name) } - it { is_expected.to include(project_id: project.id) } + context 'annotated tag' do + let(:tag_name) { Gitlab::Git.ref_name(ref) } - context "with repository data" do - subject { @push_data[:repository] } - - it { is_expected.to include(name: project.name) } - it { is_expected.to include(url: project.url_to_repo) } - it { is_expected.to include(description: project.description) } - it { is_expected.to include(homepage: project.web_url) } - end - - context "with commits" do - subject { @push_data[:commits] } - - it { is_expected.to be_an(Array) } - it 'has 1 element' do - expect(subject.size).to eq(1) + before do + service.execute + @push_data = service.push_data end - context "the commit" do - subject { @push_data[:commits].first } + it { is_expected.to include(object_kind: 'tag_push') } + it { is_expected.to include(ref: ref) } + it { is_expected.to include(before: oldrev) } + it { is_expected.to include(after: newrev) } + it { is_expected.to include(message: tag.message) } + it { is_expected.to include(user_id: user.id) } + it { is_expected.to include(user_name: user.name) } + it { is_expected.to include(project_id: project.id) } - it { is_expected.to include(id: @commit.id) } - it { is_expected.to include(message: @commit.safe_message) } - it { is_expected.to include(timestamp: @commit.date.xmlschema) } - it do - is_expected.to include( - url: [ - Gitlab.config.gitlab.url, - project.namespace.to_param, - project.to_param, - 'commit', - @commit.id - ].join('/') - ) + context "with repository data" do + subject { @push_data[:repository] } + + it { is_expected.to include(name: project.name) } + it { is_expected.to include(url: project.url_to_repo) } + it { is_expected.to include(description: project.description) } + it { is_expected.to include(homepage: project.web_url) } + end + + context "with commits" do + subject { @push_data[:commits] } + + it { is_expected.to be_an(Array) } + it 'has 1 element' do + expect(subject.size).to eq(1) end - context "with a author" do - subject { @push_data[:commits].first[:author] } + context "the commit" do + subject { @push_data[:commits].first } - it { is_expected.to include(name: @commit.author_name) } - it { is_expected.to include(email: @commit.author_email) } + it { is_expected.to include(id: commit.id) } + it { is_expected.to include(message: commit.safe_message) } + it { is_expected.to include(timestamp: commit.date.xmlschema) } + it do + is_expected.to include( + url: [ + Gitlab.config.gitlab.url, + project.namespace.to_param, + project.to_param, + 'commit', + commit.id + ].join('/') + ) + end + + context "with a author" do + subject { @push_data[:commits].first[:author] } + + it { is_expected.to include(name: commit.author_name) } + it { is_expected.to include(email: commit.author_email) } + end + end + end + end + + context 'lightweight tag' do + let(:tag_name) { 'light-tag' } + let(:newrev) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } + let(:ref) { "refs/tags/light-tag" } + + before do + # Create the lightweight tag + project.repository.raw_repository.rugged.tags.create(tag_name, newrev) + + # Clear tag list cache + project.repository.expire_tags_cache + + service.execute + @push_data = service.push_data + end + + it { is_expected.to include(object_kind: 'tag_push') } + it { is_expected.to include(ref: ref) } + it { is_expected.to include(before: oldrev) } + it { is_expected.to include(after: newrev) } + it { is_expected.to include(message: tag.message) } + it { is_expected.to include(user_id: user.id) } + it { is_expected.to include(user_name: user.name) } + it { is_expected.to include(project_id: project.id) } + + context "with repository data" do + subject { @push_data[:repository] } + + it { is_expected.to include(name: project.name) } + it { is_expected.to include(url: project.url_to_repo) } + it { is_expected.to include(description: project.description) } + it { is_expected.to include(homepage: project.web_url) } + end + + context "with commits" do + subject { @push_data[:commits] } + + it { is_expected.to be_an(Array) } + it 'has 1 element' do + expect(subject.size).to eq(1) + end + + context "the commit" do + subject { @push_data[:commits].first } + + it { is_expected.to include(id: commit.id) } + it { is_expected.to include(message: commit.safe_message) } + it { is_expected.to include(timestamp: commit.date.xmlschema) } + it do + is_expected.to include( + url: [ + Gitlab.config.gitlab.url, + project.namespace.to_param, + project.to_param, + 'commit', + commit.id + ].join('/') + ) + end + + context "with a author" do + subject { @push_data[:commits].first[:author] } + + it { is_expected.to include(name: commit.author_name) } + it { is_expected.to include(email: commit.author_email) } + end end end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 3ea1273abc..876bfaf085 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -69,7 +69,7 @@ describe Projects::CreateService, services: true do context 'wiki_enabled false does not create wiki repository directory' do before do - @opts.merge!( { project_feature_attributes: { wiki_access_level: ProjectFeature::DISABLED } }) + @opts.merge!(wiki_enabled: false) @project = create_project(@user, @opts) @path = ProjectWiki.new(@project, @user).send(:path_to_repo) end diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb index 62a5b46d47..75c95d7095 100644 --- a/spec/support/cycle_analytics_helpers.rb +++ b/spec/support/cycle_analytics_helpers.rb @@ -49,7 +49,8 @@ module CycleAnalyticsHelpers end def merge_merge_requests_closing_issue(issue) - merge_requests = issue.closed_by_merge_requests + merge_requests = issue.closed_by_merge_requests(user) + merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) } end diff --git a/spec/support/project_features_apply_to_issuables_shared_examples.rb b/spec/support/project_features_apply_to_issuables_shared_examples.rb new file mode 100644 index 0000000000..4621d17549 --- /dev/null +++ b/spec/support/project_features_apply_to_issuables_shared_examples.rb @@ -0,0 +1,56 @@ +shared_examples 'project features apply to issuables' do |klass| + let(:described_class) { klass } + + let(:group) { create(:group) } + let(:user_in_group) { create(:group_member, :developer, user: create(:user), group: group ).user } + let(:user_outside_group) { create(:user) } + + let(:project) { create(:empty_project, :public, project_args) } + + def project_args + feature = "#{described_class.model_name.plural}_access_level".to_sym + + args = { group: group } + args[feature] = access_level + + args + end + + before do + _ = issuable + login_as(user) + visit path + end + + context 'public access level' do + let(:access_level) { ProjectFeature::ENABLED } + + context 'group member' do + let(:user) { user_in_group } + + it { expect(page).to have_content(issuable.title) } + end + + context 'non-member' do + let(:user) { user_outside_group } + + it { expect(page).to have_content(issuable.title) } + end + end + + context 'private access level' do + let(:access_level) { ProjectFeature::PRIVATE } + + context 'group member' do + let(:user) { user_in_group } + + it { expect(page).to have_content(issuable.title) } + end + + context 'non-member' do + let(:user) { user_outside_group } + + it { expect(page).not_to have_content(issuable.title) } + end + end +end diff --git a/spec/support/reference_parser_shared_examples.rb b/spec/support/reference_parser_shared_examples.rb new file mode 100644 index 0000000000..8eb74635a6 --- /dev/null +++ b/spec/support/reference_parser_shared_examples.rb @@ -0,0 +1,43 @@ +RSpec.shared_examples "referenced feature visibility" do |*related_features| + let(:feature_fields) do + related_features.map { |feature| (feature + "_access_level").to_sym } + end + + before { link['data-project'] = project.id.to_s } + + context "when feature is disabled" do + it "does not create reference" do + set_features_fields_to(ProjectFeature::DISABLED) + expect(subject.nodes_visible_to_user(user, [link])).to eq([]) + end + end + + context "when feature is enabled only for team members" do + before { set_features_fields_to(ProjectFeature::PRIVATE) } + + it "does not create reference for non member" do + non_member = create(:user) + + expect(subject.nodes_visible_to_user(non_member, [link])).to eq([]) + end + + it "creates reference for member" do + project.team << [user, :developer] + + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + end + + context "when feature is enabled" do + # The project is public + it "creates reference" do + set_features_fields_to(ProjectFeature::ENABLED) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + end + + def set_features_fields_to(visibility_level) + feature_fields.each { |field| project.project_feature.update_attribute(field, visibility_level) } + end +end diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb index c8a3d02d8f..889d9a3888 100644 --- a/spec/views/projects/issues/_related_branches.html.haml_spec.rb +++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb @@ -5,7 +5,7 @@ describe 'projects/issues/_related_branches' do let(:project) { create(:project) } let(:branch) { project.repository.find_branch('feature') } - let!(:pipeline) { create(:ci_pipeline, project: project, sha: branch.target.id, ref: 'feature') } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: branch.dereferenced_target.id, ref: 'feature') } before do assign(:project, project)