diff options
216 files changed, 3534 insertions, 935 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 195783454f9..34348247e91 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -213,11 +213,24 @@ rake downtime_check: *exec rake ee_compat_check: <<: *exec only: - - branches + - branches@gitlab-org/gitlab-ce + - branches@gitlab/gitlabhq except: - master - tags + - /^[\d-]+-stable(-ee)?$/ allow_failure: yes + cache: + key: "ruby231-ee_compat_check_repo" + paths: + - ee_compat_check/repo/ + - vendor/ruby + artifacts: + name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}_${CI_BUILD_REF}" + when: on_failure + expire_in: 10d + paths: + - ee_compat_check/patches/*.patch rake db:migrate:reset: stage: test diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eefae8ed0b..86a37d5bdb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,37 @@ entry. ## 8.14.0 (2016-11-22) +- Use separate email-token for incoming email and revert back the inactive feature. !5914 +- Replace jQuery.timeago with timeago.js. !6274 (ClemMakesApps) +- Add CI notifications. Who triggered a pipeline would receive an email after the pipeline is succeeded or failed. Users could also update notification settings accordingly. !6342 +- Finer-grained Git gargage collection. !6588 +- Introduce better credential and error checking to `rake gitlab:ldap:check`. !6601 +- Process commits using a dedicated Sidekiq worker. !6802 +- Fix showing pipeline status for a given commit from correct branch. !7034 +- Add query param to filter users by external & blocked type. !7109 (Yatish Mehta) +- Issues atom feed url reflect filters on dashboard. !7114 (Lucas Deschamps) +- Add setting to only allow merge requests to be merged when all discussions are resolved. !7125 (Rodolfo Arruda) +- Remove an extra leading space from diff paste data. !7133 (Hiroyuki Sato) +- Fix 404 on network page when entering non-existent git revision. !7172 (Hiroyuki Sato) +- Rewrite git blame spinach feature tests to rspec feature tests. !7197 (Lisanne Fellinger) +- 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 +- Expose label IDs in API. !7275 (Rares Sfirlogea) +- Fix invalid filename validation on eslint. !7281 +- API: Ability to retrieve version information. !7286 (Robert Schilling) +- Set default Sidekiq retries to 3. !7294 +- Return 400 when creating a system hook fails. !7350 (Robert Schilling) +- Use the Gitlab Workhorse HTTP header in the admin dashboard. (Chris Wright) +- Add an index for project_id in project_import_data to improve performance. +- Fix broken link to observatory cli on Frontend Dev Guide. (Sam Rose) +- Faster search inside Project. +- Clicking "force remove source branch" label now toggles the checkbox again. +- Allow to test JIRA service settings without having a repository. +- Fix: Guest sees some repository details and gets 404. +- Bump omniauth-gitlab to 1.0.2 to fix incompatibility with omniauth-oauth2. +- Fix: Todos Filter Shows All Users. +- Fix broken commits search. - Show correct environment log in admin/logs (@duk3luk3 !7191) - Fix Milestone dropdown not stay selected for `Upcoming` and `No Milestone` option !7117 - Diff collapse won't shift when collapsing. @@ -12,6 +43,7 @@ entry. - Trim leading and trailing whitespace on project_path (Linus Thiel) - Prevent award emoji via notes for issues/MRs authored by user (barthc) - Adds support for the `token` attribute in project hooks API (Gauvain Pocentek) +- Change auto selection behaviour of emoji and slash commands to be more UX/Type friendly (Yann Gravrand) - Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO) - Fix Markdown styling inside reference links (Jan Zdráhal) - Create new issue board list after creating a new label @@ -74,13 +106,34 @@ entry. - Updated commit SHA styling on the branches page. - Fix 404 when visit /projects page +## 8.13.5 (2016-11-08) + +- Restore unauthenticated access to public container registries +- 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.4 + +- Pulled due to packaging error. + ## 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. - Fix relative links in Markdown wiki when displayed in "Project" tab !7218 -- Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project -- Fix project features default values - Changed build dropdown list length to be 6,5 builds long in the pipeline graph ## 8.13.2 (2016-10-31) @@ -271,6 +324,10 @@ entry. - Fix broken Project API docs (Takuya Noguchi) - Migrate invalid project members (owner -> master) +## 8.12.9 (2016-11-07) + +- Fix XSS issue in Markdown autolinker + ## 8.12.8 (2016-11-02) - Removes any symlinks before importing a project export file. CVE-2016-9086 @@ -535,6 +592,10 @@ entry. - Fix non-master branch readme display in tree view - Add UX improvements for merge request version diffs +## 8.11.11 (2016-11-07) + +- Fix XSS issue in Markdown autolinker + ## 8.11.10 (2016-11-02) - Removes any symlinks before importing a project export file. CVE-2016-9086 diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index e6b55c9b6ae..5c047dd4481 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -58,11 +58,28 @@ document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch); window.addEventListener('hashchange', gl.utils.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'); + + 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(gl.utils.shiftWindow, 100); + return setTimeout(adjustScroll, 100); } }; @@ -193,9 +210,6 @@ e.preventDefault(); return new ConfirmDangerModal(form, text); }); - $document.on('click', 'button', function () { - return $(this).blur(); - }); $('input[type="search"]').each(function () { var $this = $(this); $this.attr('value', $this.val()); diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 4ddafff428f..82bfdcea0ca 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -43,10 +43,6 @@ bottom: unfoldBottom, offset: offset, unfold: unfold, - // indent is used to compensate for single space indent to fit - // '+' and '-' prepended to diff lines, - // see https://gitlab.com/gitlab-org/gitlab-ce/issues/707 - indent: 1, view: file.data('view') }; return $.get(link, params, function(response) { diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 824413bf20f..e72e2194be8 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -34,6 +34,8 @@ }, DefaultOptions: { sorter: function(query, items, searchKey) { + // Highlight first item only if at least one char was typed + this.setting.highlightFirst = query.length > 0; if ((items[0].name != null) && items[0].name === 'loading') { return items; } @@ -182,6 +184,7 @@ insertTpl: '${atwho-at}"${title}"', data: ['loading'], callbacks: { + sorter: this.DefaultOptions.sorter, beforeSave: function(milestones) { return $.map(milestones, function(m) { if (m.title == null) { @@ -236,6 +239,7 @@ displayTpl: this.Labels.template, insertTpl: '${atwho-at}${title}', callbacks: { + sorter: this.DefaultOptions.sorter, beforeSave: function(merges) { var sanitizeLabelTitle; sanitizeLabelTitle = function(title) { diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 860ee5df57e..f06b10a9cf7 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -130,7 +130,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/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index ed21ad83a1c..e7aff2d0cec 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -6,7 +6,6 @@ &:focus, &:active { - outline: none; background-color: $btn-active-gray; box-shadow: $gl-btn-active-background; } @@ -267,10 +266,6 @@ outline: none; } - &:focus { - outline: none; - } - &:active { outline: none; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 3e34ec98427..583c17e4a83 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -38,7 +38,6 @@ text-align: left; border: 1px solid $border-color; border-radius: $border-radius-base; - outline: 0; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; @@ -55,6 +54,10 @@ } } + &.no-outline { + outline: 0; + } + &:hover, { border-color: $dropdown-toggle-hover-border-color; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 4993ca7572a..5a34132112a 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -100,10 +100,6 @@ header { &:hover { background-color: $btn-gray-hover; } - - &:focus { - outline: none; - } } } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index fcaf5e18633..ce864c2de5e 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -58,7 +58,6 @@ &:active, &:focus { text-decoration: none; - outline: none; } } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index d74c14ee2a4..44c445c0543 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -83,7 +83,6 @@ display: block; text-decoration: none; font-weight: normal; - outline: none; &:hover, &:active, diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 98a84351a3d..61fbd7425b7 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -62,6 +62,8 @@ .ci-status-link { display: inline-block; + position: relative; + top: 1px; } .btn-clipboard, diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index fde138c874d..99fdea15218 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -92,20 +92,6 @@ &.noteable_line { position: relative; - - &.old { - &::before { - content: '-'; - position: absolute; - } - } - - &.new { - &::before { - content: '+'; - position: absolute; - } - } } span { @@ -151,8 +137,9 @@ .line_content { display: block; margin: 0; - padding: 0 0.5em; + padding: 0 1.5em; border: none; + position: relative; &.parallel { display: table-cell; @@ -161,6 +148,22 @@ word-break: break-all; } } + + &.old { + &::before { + content: '-'; + position: absolute; + left: 0.5em; + } + } + + &.new { + &::before { + content: '+'; + position: absolute; + left: 0.5em; + } + } } .text-file.diff-wrap-lines table .line_holder td span { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 230b927a17d..773155fe80a 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -267,20 +267,6 @@ } } - .issuable-header-btn { - background: $gray-normal; - border: 1px solid $border-gray-normal; - - &:hover { - background: $gray-dark; - border: 1px solid $border-gray-dark; - } - - &.btn-primary { - @extend .btn-primary; - } - } - a { &:hover { color: $md-link-color; diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 032feae8854..19ab198c2e7 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -228,7 +228,6 @@ $colors: ( position: absolute; right: 10px; padding: 0; - outline: none; color: #fff; width: 75px; // static width to make 2 buttons have same width height: 19px; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index f8e31a624ec..6cf43713fec 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -47,6 +47,7 @@ &.right { float: right; + padding-right: 0; a { color: $gl-gray; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index bf688af50e2..b4761df3f23 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -31,7 +31,6 @@ padding-right: 20px; border: none; font-size: 14px; - outline: none; padding: 0; margin-left: 5px; line-height: 25px; @@ -229,6 +228,5 @@ &:hover, &:focus { color: $gl-link-color; - outline: none; } } diff --git a/app/controllers/concerns/diff_for_path.rb b/app/controllers/concerns/diff_for_path.rb index aeec3009f15..1efa9fe060f 100644 --- a/app/controllers/concerns/diff_for_path.rb +++ b/app/controllers/concerns/diff_for_path.rb @@ -3,7 +3,7 @@ module DiffForPath def render_diff_for_path(diffs) diff_file = diffs.diff_files.find do |diff| - diff.old_path == params[:old_path] && diff.new_path == params[:new_path] + diff.file_identifier == params[:file_identifier] end return render_404 unless diff_file diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 9d5a28e8d4d..506484932cc 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -58,7 +58,7 @@ class Groups::MilestonesController < Groups::ApplicationController def render_new_with_error(empty_project_ids) @milestone = Milestone.new(milestone_params) - @milestone.errors.add(:project_id, "Please select at least one project.") if empty_project_ids + @milestone.errors.add(:base, "Please select at least one project.") if empty_project_ids render :new end diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 7e4da73bc11..c736200a104 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/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 383e184d796..3f41916e6d3 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 662d38b10a5..13caeb42d40 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/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 371cc3787fb..533af80aee0 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -18,7 +18,9 @@ class Projects::PipelinesController < Projects::ApplicationController end def create - @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute(ignore_skip_ci: true, save_on_errors: false) + @pipeline = Ci::CreatePipelineService + .new(project, current_user, create_params) + .execute(ignore_skip_ci: true, save_on_errors: false) unless @pipeline.persisted? render 'new' return diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 6a881b271d7..c4508ccc3b9 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 cc2073081b5..6297b2db369 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 diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 0725c3f4c56..f489f9aa0d6 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -51,12 +51,11 @@ module DiffHelper html.html_safe end - def diff_line_content(line, line_type = nil) + def diff_line_content(line) if line.blank? - " ".html_safe + " ".html_safe else - line[0] = ' ' if %w[new old].include?(line_type) - line + line.sub(/^[\-+ ]/, '').html_safe end end diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb index 95b60aeab5f..d3966ba1f10 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,7 @@ 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 user_can_download_code? diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 7e8369d0a05..03cc8f2b6bd 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -74,4 +74,13 @@ module NotificationsHelper return unless notification_setting.source_type hidden_field_tag "#{notification_setting.source_type.downcase}_id", notification_setting.source_id end + + def notification_event_name(event) + case event + when :success_pipeline + 'Successful pipeline' + else + event.to_s.humanize + end + end end diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb index 601c8b5cd62..9460a6cd2be 100644 --- a/app/mailers/emails/pipelines.rb +++ b/app/mailers/emails/pipelines.rb @@ -1,22 +1,27 @@ module Emails module Pipelines - def pipeline_success_email(pipeline, to) - pipeline_mail(pipeline, to, 'succeeded') + def pipeline_success_email(pipeline, recipients) + pipeline_mail(pipeline, recipients, 'succeeded') end - def pipeline_failed_email(pipeline, to) - pipeline_mail(pipeline, to, 'failed') + def pipeline_failed_email(pipeline, recipients) + pipeline_mail(pipeline, recipients, 'failed') end private - def pipeline_mail(pipeline, to, status) + def pipeline_mail(pipeline, recipients, status) @project = pipeline.project @pipeline = pipeline @merge_request = pipeline.merge_requests.first add_headers - mail(to: to, subject: pipeline_subject(status), skip_premailer: true) do |format| + # We use bcc here because we don't want to generate this emails for a + # thousand times. This could be potentially expensive in a loop, and + # recipients would contain all project watchers so it could be a lot. + mail(bcc: recipients, + subject: pipeline_subject(status), + skip_premailer: true) do |format| format.html { render layout: false } format.text end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index d3432632899..3fee6c18770 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -81,6 +81,12 @@ module Ci PipelineHooksWorker.perform_async(id) end end + + after_transition any => [:success, :failed] do |pipeline| + pipeline.run_after_commit do + PipelineNotificationWorker.perform_async(pipeline.id) + end + end end # ref can't be HEAD or SHA, can only be branch/tag name @@ -109,6 +115,11 @@ module Ci project.id end + # For now the only user who participates is the user who triggered + def participants(_current_user = nil) + Array(user) + end + def valid_commit_sha if self.sha == Gitlab::Git::BLANK_SHA self.errors.add(:sha, " cant be 00000000 (branch removal)") diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 93a6b3122e0..664bb594aa9 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -183,6 +183,10 @@ module Issuable grouping_columns end + + def to_ability_name + model_name.singular + end end def today? @@ -244,7 +248,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/event.rb b/app/models/event.rb index 43e67069b70..c76d88b1c7b 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -49,6 +49,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"], @@ -62,7 +63,7 @@ class Event < ActiveRecord::Base def visible_to_user?(user = nil) if push? - true + Ability.allowed?(user, :download_code, project) elsif membership_changed? true elsif created_project? diff --git a/app/models/guest.rb b/app/models/guest.rb new file mode 100644 index 00000000000..01285ca1264 --- /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 4f02b02c488..adbca510ef7 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -250,29 +250,9 @@ 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) - 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 + return false unless project.feature_available?(:issues, user) - # Returns `true` if this Issue is visible to everybody. - def publicly_visible? - project.public? && !confidential? + user ? readable_by?(user) : publicly_visible? end def overdue? @@ -297,4 +277,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/notification_setting.rb b/app/models/notification_setting.rb index 121b598b8f3..43fc218de2b 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -32,7 +32,9 @@ class NotificationSetting < ActiveRecord::Base :reopen_merge_request, :close_merge_request, :reassign_merge_request, - :merge_merge_request + :merge_merge_request, + :failed_pipeline, + :success_pipeline ] store :events, accessors: EMAIL_EVENTS, coder: JSON diff --git a/app/models/project.rb b/app/models/project.rb index 4c9c7c001dd..bbe590b5a8a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -207,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) } diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index b37ce1d3cf6..34fd5a57b5e 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_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb index ec3c1bc85ee..745f9bd1b43 100644 --- a/app/models/project_services/pipelines_email_service.rb +++ b/app/models/project_services/pipelines_email_service.rb @@ -1,10 +1,7 @@ class PipelinesEmailService < Service prop_accessor :recipients - boolean_accessor :add_pusher boolean_accessor :notify_only_broken_pipelines - validates :recipients, - presence: true, - if: ->(s) { s.activated? && !s.add_pusher? } + validates :recipients, presence: true, if: :activated? def initialize_properties self.properties ||= { notify_only_broken_pipelines: true } @@ -34,8 +31,8 @@ class PipelinesEmailService < Service return unless all_recipients.any? - pipeline = Ci::Pipeline.find(data[:object_attributes][:id]) - Ci::SendPipelineNotificationService.new(pipeline).execute(all_recipients) + pipeline_id = data[:object_attributes][:id] + PipelineNotificationWorker.new.perform(pipeline_id, all_recipients) end def can_test? @@ -58,9 +55,6 @@ class PipelinesEmailService < Service name: 'recipients', placeholder: 'Emails separated by comma' }, { type: 'checkbox', - name: 'add_pusher', - label: 'Add pusher to recipients list' }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, ] end @@ -85,12 +79,6 @@ class PipelinesEmailService < Service end def retrieve_recipients(data) - all_recipients = recipients.to_s.split(',').reject(&:blank?) - - if add_pusher? && data[:user].try(:[], :email) - all_recipients << data[:user][:email] - end - - all_recipients + recipients.to_s.split(',').reject(&:blank?) end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 7d06ce1e85b..063dc74021d 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -84,15 +84,17 @@ class Repository def commit(ref = 'HEAD') return nil unless exists? + commit = if ref.is_a?(Gitlab::Git::Commit) ref else Gitlab::Git::Commit.find(raw_repository, ref) end + commit = ::Commit.new(commit, @project) if commit commit - rescue Rugged::OdbError + rescue Rugged::OdbError, Rugged::TreeError nil end @@ -232,6 +234,8 @@ class Repository def ref_exists?(ref) rugged.references.exist?(ref) + rescue Rugged::ReferenceError + false end def update_ref!(name, newrev, oldrev) @@ -239,7 +243,7 @@ class Repository # offer 'compare and swap' ref updates. Without compare-and-swap we can # (and have!) accidentally reset the ref to an earlier state, clobbering # commits. See also https://github.com/libgit2/libgit2/issues/1534. - command = %w[git update-ref --stdin -z] + command = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z) _, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin| stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00") end @@ -270,11 +274,7 @@ class Repository end def kept_around?(sha) - begin - ref_exists?(keep_around_ref_name(sha)) - rescue Rugged::ReferenceError - false - end + ref_exists?(keep_around_ref_name(sha)) end def tag_names diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 2232e231cf8..8b25332b73c 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -5,7 +5,7 @@ module Ci # If we can't read build we should also not have that # ability when looking at this in context of commit_status - %w(read create update admin).each do |rule| + %w[read create update admin].each do |rule| cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build" end end diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb new file mode 100644 index 00000000000..3d2eef1c50c --- /dev/null +++ b/app/policies/ci/pipeline_policy.rb @@ -0,0 +1,4 @@ +module Ci + class PipelinePolicy < BuildPolicy + end +end diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb index 52fa33bc4b0..88f3179c6ff 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/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 8ea88da8a53..c00c5aebf57 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/ci/send_pipeline_notification_service.rb b/app/services/ci/send_pipeline_notification_service.rb deleted file mode 100644 index ceb182801f7..00000000000 --- a/app/services/ci/send_pipeline_notification_service.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Ci - class SendPipelineNotificationService - attr_reader :pipeline - - def initialize(new_pipeline) - @pipeline = new_pipeline - end - - def execute(recipients) - email_template = "pipeline_#{pipeline.status}_email" - - return unless Notify.respond_to?(email_template) - - recipients.each do |to| - Notify.public_send(email_template, pipeline, to).deliver_later - end - end - end -end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 72712afc07e..6697840cc26 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -312,6 +312,22 @@ class NotificationService mailer.project_was_not_exported_email(current_user, project, errors).deliver_later end + def pipeline_finished(pipeline, recipients = nil) + email_template = "pipeline_#{pipeline.status}_email" + + return unless mailer.respond_to?(email_template) + + recipients ||= build_recipients( + pipeline, + pipeline.project, + nil, # The acting user, who won't be added to recipients + action: pipeline.status).map(&:notification_email) + + if recipients.any? + mailer.public_send(email_template, pipeline, recipients).deliver_later + end + end + protected # Get project/group users with CUSTOM notification level @@ -475,9 +491,14 @@ class NotificationService end def reject_users_without_access(recipients, target) - return recipients unless target.is_a?(Issuable) + ability = case target + when Issuable + :"read_#{target.to_ability_name}" + when Ci::Pipeline + :read_build # We have build trace in pipeline emails + end - ability = :"read_#{target.to_ability_name}" + return recipients unless ability recipients.select do |user| user.can?(ability, target) @@ -624,6 +645,6 @@ class NotificationService # Build event key to search on custom notification level # Check NotificationSetting::EMAIL_EVENTS def build_custom_key(action, object) - "#{action}_#{object.class.name.underscore}".to_sym + "#{action}_#{object.class.model_name.name.underscore}".to_sym end end diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml index 1e957f0935f..aec1b31ce62 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/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index 23d438b2aa1..0dfaf743992 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -34,7 +34,7 @@ = f.label :projects, "Projects", class: "control-label" .col-sm-10 = f.collection_select :project_ids, @group.projects.non_archived, :id, :name, - { selected: @group.projects.non_archived.pluck(:id) }, multiple: true, class: 'select2' + { selected: @group.projects.non_archived.pluck(:id) }, required: true, multiple: true, class: 'select2' .col-md-6 .form-group diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index d7386105b7d..8e65bd12c56 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -13,7 +13,7 @@ .location-badge= label .search-input-wrap .dropdown{ data: { url: search_autocomplete_path } } - = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url } + = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url } .dropdown-menu.dropdown-select = dropdown_content do %ul diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index 75275afc0f3..c0328fe8842 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -14,7 +14,7 @@ - if can_admin_group = nav_link(path: 'groups#projects') do = link_to 'Projects', projects_group_path(@group), title: 'Projects' - - if can_edit || can_leave + - if (can_edit || can_leave) && can_admin_group %li.divider - if can_edit %li diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index d6916fb7f1a..062a8905a19 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -1,7 +1,7 @@ .pipeline-graph-container .row-content-block.build-content.middle-block.pipeline-actions .pull-right - .btn.btn-grouped.btn-white.toggle-pipeline-btn + %button.btn.btn-grouped.btn-white.toggle-pipeline-btn %span.toggle-btn-text Hide %span pipeline graph %span.caret diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 9f80a974d64..34855c54176 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -6,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}" } diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml index 779c8ea0104..c3d2f80544b 100644 --- a/app/views/projects/diffs/_content.html.haml +++ b/app/views/projects/diffs/_content.html.haml @@ -9,7 +9,7 @@ - if !project.repository.diffable?(blob) .nothing-here-block This diff was suppressed by a .gitattributes entry. - elsif diff_file.collapsed? - - url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path)) + - url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier)) .nothing-here-block.diff-collapsed{data: { diff_for_path: url } } This diff is collapsed. %a.click-to-expand diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 7042e9f1fc9..a3e4b5b777e 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -25,9 +25,9 @@ %a{href: "##{line_code}", data: { linenumber: link_text }} %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }< - if email - %pre= diff_line_content(line.text, type) + %pre= diff_line_content(line.text) - else - = diff_line_content(line.text, type) + = diff_line_content(line.text) - discussions = local_assigns.fetch(:discussions, nil) - if discussions && !line.meta? diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml index 4d8ee562e6a..c52b3860636 100644 --- a/app/views/projects/imports/show.html.haml +++ b/app/views/projects/imports/show.html.haml @@ -1,4 +1,4 @@ -- page_title "Import in progress" +- page_title @project.forked? ? "Forking in progress" : "Import in progress" .save-project-loader .center %h2 diff --git a/app/views/shared/icons/_icon_status_skipped.svg b/app/views/shared/icons/_icon_status_skipped.svg index 014ca86b61b..3420af411f6 100644 --- a/app/views/shared/icons/_icon_status_skipped.svg +++ b/app/views/shared/icons/_icon_status_skipped.svg @@ -1 +1 @@ -<svg width="20" height="20" class="ci-status-icon-skipped" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Group Copy 31</title><g fill="#5C5C5C" fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg> +<svg width="14" height="14" class="ci-status-icon-skipped" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Group Copy 31</title><g fill="#5C5C5C" fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg> diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml index b704981e3db..a82fc95df84 100644 --- a/app/views/shared/notifications/_custom_notifications.html.haml +++ b/app/views/shared/notifications/_custom_notifications.html.haml @@ -27,5 +27,5 @@ %label{ for: field_id } = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event]) %strong - = event.to_s.humanize + = notification_event_name(event) = icon("spinner spin", class: "custom-notification-event-loading") diff --git a/app/workers/pipeline_notification_worker.rb b/app/workers/pipeline_notification_worker.rb new file mode 100644 index 00000000000..cdb860b6675 --- /dev/null +++ b/app/workers/pipeline_notification_worker.rb @@ -0,0 +1,12 @@ +class PipelineNotificationWorker + include Sidekiq::Worker + include PipelineQueue + + def perform(pipeline_id, recipients = nil) + pipeline = Ci::Pipeline.find_by(id: pipeline_id) + + return unless pipeline + + NotificationService.new.pipeline_finished(pipeline, recipients) + end +end diff --git a/changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml b/changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml deleted file mode 100644 index 8f03746ff80..00000000000 --- a/changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add setting to only allow merge requests to be merged when all discussions are resolved -merge_request: 7125 -author: Rodolfo Arruda diff --git a/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml b/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml deleted file mode 100644 index 95d8fef1099..00000000000 --- a/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use the Gitlab Workhorse HTTP header in the admin dashboard -merge_request: -author: Chris Wright diff --git a/changelogs/unreleased/22588-todos-filter-shows-all-users.yml b/changelogs/unreleased/22588-todos-filter-shows-all-users.yml deleted file mode 100644 index 1da72142880..00000000000 --- a/changelogs/unreleased/22588-todos-filter-shows-all-users.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Fix: Todos Filter Shows All Users' -merge_request: -author: diff --git a/changelogs/unreleased/22699-group-permssion-background-migration.yml b/changelogs/unreleased/22699-group-permssion-background-migration.yml new file mode 100644 index 00000000000..e8c221b6c42 --- /dev/null +++ b/changelogs/unreleased/22699-group-permssion-background-migration.yml @@ -0,0 +1,4 @@ +--- +title: Fix project records with invalid visibility_level values +merge_request: 7391 +author: diff --git a/changelogs/unreleased/22947-fix_issues_atom_feed_url.yml b/changelogs/unreleased/22947-fix_issues_atom_feed_url.yml deleted file mode 100644 index 2312afdb3d7..00000000000 --- a/changelogs/unreleased/22947-fix_issues_atom_feed_url.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Issues atom feed url reflect filters on dashboard -merge_request: 7114 -author: Lucas Deschamps diff --git a/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml b/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml deleted file mode 100644 index 7b54d3df56d..00000000000 --- a/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Rewrite git blame spinach feature tests to rspec feature tests -merge_request: 7197 -author: Lisanne Fellinger diff --git a/changelogs/unreleased/23961-can-t-share-project-with-groups.yml b/changelogs/unreleased/23961-can-t-share-project-with-groups.yml deleted file mode 100644 index b3bfcbda4b7..00000000000 --- a/changelogs/unreleased/23961-can-t-share-project-with-groups.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Only skip group when it's actually a group in the "Share with group" select -merge_request: 7262 -author: diff --git a/changelogs/unreleased/24038-fix-no-register-pane-if-ldap.yml b/changelogs/unreleased/24038-fix-no-register-pane-if-ldap.yml new file mode 100644 index 00000000000..53f418b6b18 --- /dev/null +++ b/changelogs/unreleased/24038-fix-no-register-pane-if-ldap.yml @@ -0,0 +1,4 @@ +--- +title: Fix no "Register" tab if ldap auth is enabled (#24038) +merge_request: 7274 +author: Luc Didry diff --git a/changelogs/unreleased/24048-dropdown-issue-with-devider.yml b/changelogs/unreleased/24048-dropdown-issue-with-devider.yml new file mode 100644 index 00000000000..b889da61957 --- /dev/null +++ b/changelogs/unreleased/24048-dropdown-issue-with-devider.yml @@ -0,0 +1,4 @@ +--- +title: "[Fix] Extra divider issue in dropdown" +merge_request: 7398 +author: diff --git a/changelogs/unreleased/24056-guest-sees-some-project-details-and-gets-404.yml b/changelogs/unreleased/24056-guest-sees-some-project-details-and-gets-404.yml deleted file mode 100644 index 8ca0c5beab3..00000000000 --- a/changelogs/unreleased/24056-guest-sees-some-project-details-and-gets-404.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Fix: Guest sees some repository details and gets 404' -merge_request: -author: diff --git a/changelogs/unreleased/24059-round-robin-repository-storage.yml b/changelogs/unreleased/24059-round-robin-repository-storage.yml deleted file mode 100644 index 109536114ff..00000000000 --- a/changelogs/unreleased/24059-round-robin-repository-storage.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Introduce round-robin project creation to spread load over multiple shards -merge_request: 7266 -author: diff --git a/changelogs/unreleased/24102-cannot-unselect-remove-source-branch-when-editing-merge-request.yml b/changelogs/unreleased/24102-cannot-unselect-remove-source-branch-when-editing-merge-request.yml deleted file mode 100644 index 50d018170f1..00000000000 --- a/changelogs/unreleased/24102-cannot-unselect-remove-source-branch-when-editing-merge-request.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Ensure merge request's "remove branch" accessors return booleans -merge_request: 7267 -author: diff --git a/changelogs/unreleased/24255-search-fix.yml b/changelogs/unreleased/24255-search-fix.yml deleted file mode 100644 index c0afade9bc8..00000000000 --- a/changelogs/unreleased/24255-search-fix.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix broken commits search -merge_request: -author: diff --git a/changelogs/unreleased/24279-issue-merge-request-sidebar-todo-button-style-improvement.yml b/changelogs/unreleased/24279-issue-merge-request-sidebar-todo-button-style-improvement.yml new file mode 100644 index 00000000000..72e7110d1b8 --- /dev/null +++ b/changelogs/unreleased/24279-issue-merge-request-sidebar-todo-button-style-improvement.yml @@ -0,0 +1,4 @@ +--- +title: Removed gray button styling from todo buttons in sidebars +merge_request: 7387 +author: diff --git a/changelogs/unreleased/24369-remove-additional-padding.yml b/changelogs/unreleased/24369-remove-additional-padding.yml new file mode 100644 index 00000000000..a6a0b248412 --- /dev/null +++ b/changelogs/unreleased/24369-remove-additional-padding.yml @@ -0,0 +1,4 @@ +--- +title: Remove additional padding on right-aligned items in MR widget. +merge_request: 7411 +author: Didem Acet diff --git a/changelogs/unreleased/adam-fix-collapsed-diff-symlink-file-conversion.yml b/changelogs/unreleased/adam-fix-collapsed-diff-symlink-file-conversion.yml new file mode 100644 index 00000000000..c83558f33d1 --- /dev/null +++ b/changelogs/unreleased/adam-fix-collapsed-diff-symlink-file-conversion.yml @@ -0,0 +1,4 @@ +--- +title: Fix expanding a collapsed diff when converting a symlink to a regular file +merge_request: 6953 +author: diff --git a/changelogs/unreleased/add-api-label-id.yml b/changelogs/unreleased/add-api-label-id.yml deleted file mode 100644 index 3af4f5e677d..00000000000 --- a/changelogs/unreleased/add-api-label-id.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Expose label IDs in API -merge_request: 7275 -author: Rares Sfirlogea diff --git a/changelogs/unreleased/add-project-import-data-index.yml b/changelogs/unreleased/add-project-import-data-index.yml deleted file mode 100644 index f5e4005f544..00000000000 --- a/changelogs/unreleased/add-project-import-data-index.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add an index for project_id in project_import_data to improve performance -merge_request: -author: diff --git a/changelogs/unreleased/api-label-priorities.yml b/changelogs/unreleased/api-label-priorities.yml deleted file mode 100644 index 85b6c2761bb..00000000000 --- a/changelogs/unreleased/api-label-priorities.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: API: Ability to retrieve version information -merge_request: 7286 -author: Robert Schilling diff --git a/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml b/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml deleted file mode 100644 index d132d7e79c3..00000000000 --- a/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Return 400 when creating a system hook fails -merge_request: 7350 -author: Robert Schilling diff --git a/changelogs/unreleased/broken-link-frontend-dev-guide.yml b/changelogs/unreleased/broken-link-frontend-dev-guide.yml deleted file mode 100644 index d7b6f4a7701..00000000000 --- a/changelogs/unreleased/broken-link-frontend-dev-guide.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix broken link to observatory cli on Frontend Dev Guide -merge_request: -author: Sam Rose diff --git a/changelogs/unreleased/faster_project_search.yml b/changelogs/unreleased/faster_project_search.yml deleted file mode 100644 index e29a9f34ed4..00000000000 --- a/changelogs/unreleased/faster_project_search.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Faster search inside Project -merge_request: -author: diff --git a/changelogs/unreleased/feature-api_owned_resource.yml b/changelogs/unreleased/feature-api_owned_resource.yml new file mode 100644 index 00000000000..9c270e4ecf4 --- /dev/null +++ b/changelogs/unreleased/feature-api_owned_resource.yml @@ -0,0 +1,4 @@ +--- +title: Add api endpoint `/groups/owned` +merge_request: 7103 +author: Borja Aparicio diff --git a/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml b/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml deleted file mode 100644 index d1bc8ea2eb1..00000000000 --- a/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix 404 on network page when entering non-existent git revision -merge_request: 7172 -author: Hiroyuki Sato diff --git a/changelogs/unreleased/fix-cache-for-commit-status.yml b/changelogs/unreleased/fix-cache-for-commit-status.yml new file mode 100644 index 00000000000..eb4e96e75ae --- /dev/null +++ b/changelogs/unreleased/fix-cache-for-commit-status.yml @@ -0,0 +1,4 @@ +--- +title: Fix cache for commit status in commits list to respect branches +merge_request: 7372 +author: diff --git a/changelogs/unreleased/fix-error-when-invalid-branch-for-new-pipeline-used.yml b/changelogs/unreleased/fix-error-when-invalid-branch-for-new-pipeline-used.yml new file mode 100644 index 00000000000..ad6aa214f0f --- /dev/null +++ b/changelogs/unreleased/fix-error-when-invalid-branch-for-new-pipeline-used.yml @@ -0,0 +1,4 @@ +--- +title: Fix error when using invalid branch name when creating a new pipeline +merge_request: 7324 +author: diff --git a/changelogs/unreleased/fix-invalid-filename-eslint.yml b/changelogs/unreleased/fix-invalid-filename-eslint.yml deleted file mode 100644 index eea21149c90..00000000000 --- a/changelogs/unreleased/fix-invalid-filename-eslint.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix invalid filename validation on eslint -merge_request: 7281 -author: diff --git a/changelogs/unreleased/fix-uncheckable-label-for-force_remove_source_branch.yml b/changelogs/unreleased/fix-uncheckable-label-for-force_remove_source_branch.yml deleted file mode 100644 index 8b41063151b..00000000000 --- a/changelogs/unreleased/fix-uncheckable-label-for-force_remove_source_branch.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Clicking "force remove source branch" label now toggles the checkbox again -merge_request: -author: diff --git a/changelogs/unreleased/forking-in-progress-title.yml b/changelogs/unreleased/forking-in-progress-title.yml new file mode 100644 index 00000000000..4b9684844b3 --- /dev/null +++ b/changelogs/unreleased/forking-in-progress-title.yml @@ -0,0 +1,4 @@ +--- +title: Use 'Forking in progress' title when appropriate +merge_request: 7394 +author: Philip Karpiak diff --git a/changelogs/unreleased/git-gc-improvements.yml b/changelogs/unreleased/git-gc-improvements.yml deleted file mode 100644 index f15e667ce87..00000000000 --- a/changelogs/unreleased/git-gc-improvements.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Finer-grained Git gargage collection -merge_request: 6588 -author: diff --git a/changelogs/unreleased/issue_23032.yml b/changelogs/unreleased/issue_23032.yml deleted file mode 100644 index d376cf52112..00000000000 --- a/changelogs/unreleased/issue_23032.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow to test JIRA service settings without having a repository -merge_request: -author: diff --git a/changelogs/unreleased/milestone-project-require.yml b/changelogs/unreleased/milestone-project-require.yml new file mode 100644 index 00000000000..e43033541c7 --- /dev/null +++ b/changelogs/unreleased/milestone-project-require.yml @@ -0,0 +1,4 @@ +--- +title: Require projects before creating milestone. +merge_request: 7301 +author: gfyoung diff --git a/changelogs/unreleased/process-commits-using-sidekiq.yml b/changelogs/unreleased/process-commits-using-sidekiq.yml deleted file mode 100644 index 9f596e6a584..00000000000 --- a/changelogs/unreleased/process-commits-using-sidekiq.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Process commits using a dedicated Sidekiq worker -merge_request: 6802 -author: diff --git a/changelogs/unreleased/sh-bump-omniauth-gitlab.yml b/changelogs/unreleased/sh-bump-omniauth-gitlab.yml deleted file mode 100644 index 17cd5a993dd..00000000000 --- a/changelogs/unreleased/sh-bump-omniauth-gitlab.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Bump omniauth-gitlab to 1.0.2 to fix incompatibility with omniauth-oauth2 -merge_request: -author: diff --git a/changelogs/unreleased/show-status-from-branch.yml b/changelogs/unreleased/show-status-from-branch.yml deleted file mode 100644 index 1afc230c05c..00000000000 --- a/changelogs/unreleased/show-status-from-branch.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix showing pipeline status for a given commit from correct branch -merge_request: 7034 -author: diff --git a/changelogs/unreleased/sidekiq_default_retries.yml b/changelogs/unreleased/sidekiq_default_retries.yml deleted file mode 100644 index 3df2a415dbc..00000000000 --- a/changelogs/unreleased/sidekiq_default_retries.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Set default Sidekiq retries to 3 -merge_request: 7294 -author: diff --git a/changelogs/unreleased/upgrade-timeago.yml b/changelogs/unreleased/upgrade-timeago.yml deleted file mode 100644 index ddb266ba558..00000000000 --- a/changelogs/unreleased/upgrade-timeago.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace jQuery.timeago with timeago.js -merge_request: 6274 -author: ClemMakesApps diff --git a/changelogs/unreleased/use-separate-token-for-incoming-email.yml b/changelogs/unreleased/use-separate-token-for-incoming-email.yml deleted file mode 100644 index e498f8dd0a6..00000000000 --- a/changelogs/unreleased/use-separate-token-for-incoming-email.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use separate email-token for incoming email and revert back the inactive feature -merge_request: 5914 -author: diff --git a/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb new file mode 100644 index 00000000000..bea1cfa4c5d --- /dev/null +++ b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb @@ -0,0 +1,49 @@ +class FixProjectRecordsWithInvalidVisibility < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + BATCH_SIZE = 1000 + DOWNTIME = false + + # This migration is idempotent and there's no sense in throwing away the + # partial result if it's interrupted + disable_ddl_transaction! + + def up + projects = Arel::Table.new(:projects) + namespaces = Arel::Table.new(:namespaces) + + finder = + projects. + join(namespaces, Arel::Nodes::InnerJoin). + on(projects[:namespace_id].eq(namespaces[:id])). + where(projects[:visibility_level].gt(namespaces[:visibility_level])). + project(projects[:id]). + take(BATCH_SIZE) + + # MySQL requires a derived table to perform this query + nested_finder = + projects. + from(finder.as("AS projects_inner")). + project(projects[:id]) + + valuer = + namespaces. + where(namespaces[:id].eq(projects[:namespace_id])). + project(namespaces[:visibility_level]) + + # Update matching rows until none remain. The finder contains a limit. + loop do + updater = Arel::UpdateManager.new(ActiveRecord::Base). + table(projects). + set(projects[:visibility_level] => Arel::Nodes::SqlLiteral.new("(#{valuer.to_sql})")). + where(projects[:id].in(nested_finder)) + + num_updated = connection.exec_update(updater.to_sql, self.class.name, []) + break if num_updated == 0 + end + end + + def down + # no-op + end +end diff --git a/db/schema.rb b/db/schema.rb index 62c325a52d7..64d744d8268 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: 20161106185620) do +ActiveRecord::Schema.define(version: 20161109150329) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/doc/README.md b/doc/README.md index c30bf328003..66c8c26e4f0 100644 --- a/doc/README.md +++ b/doc/README.md @@ -21,6 +21,7 @@ - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. - [University](university/README.md) Learn Git and GitLab through videos and courses. - [Git Attributes](user/project/git_attributes.md) Managing Git attributes using a `.gitattributes` file. +- [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf) Download a PDF describing the most used Git operations. ## Administrator documentation diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index bf7814875bf..fd23047f027 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -35,6 +35,10 @@ of one hour. To enable LDAP integration you need to add your LDAP server settings in `/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`. +There is a Rake task to check LDAP configuration. After configuring LDAP +using the documentation below, see [LDAP check Rake task](../raketasks/check.md#ldap-check) +for information on the LDAP check Rake task. + >**Note**: In GitLab EE, you can configure multiple LDAP servers to connect to one GitLab server. diff --git a/doc/raketasks/check_repos_output.png b/doc/administration/img/raketasks/check_repos_output.png Binary files differindex 1f632566b00..1f632566b00 100644 --- a/doc/raketasks/check_repos_output.png +++ b/doc/administration/img/raketasks/check_repos_output.png diff --git a/doc/administration/raketasks/check.md b/doc/administration/raketasks/check.md new file mode 100644 index 00000000000..d1d2fed4861 --- /dev/null +++ b/doc/administration/raketasks/check.md @@ -0,0 +1,97 @@ +# Check Rake Tasks + +## Repository Integrity + +Even though Git is very resilient and tries to prevent data integrity issues, +there are times when things go wrong. The following Rake tasks intend to +help GitLab administrators diagnose problem repositories so they can be fixed. + +There are 3 things that are checked to determine integrity. + +1. Git repository file system check ([git fsck](https://git-scm.com/docs/git-fsck)). + This step verifies the connectivity and validity of objects in the repository. +1. Check for `config.lock` in the repository directory. +1. Check for any branch/references lock files in `refs/heads`. + +It's important to note that the existence of `config.lock` or reference locks +alone do not necessarily indicate a problem. Lock files are routinely created +and removed as Git and GitLab perform operations on the repository. They serve +to prevent data integrity issues. However, if a Git operation is interrupted these +locks may not be cleaned up properly. + +The following symptoms may indicate a problem with repository integrity. If users +experience these symptoms you may use the rake tasks described below to determine +exactly which repositories are causing the trouble. + +- Receiving an error when trying to push code - `remote: error: cannot lock ref` +- A 500 error when viewing the GitLab dashboard or when accessing a specific project. + +### Check all GitLab repositories + +This task loops through all repositories on the GitLab server and runs the +3 integrity checks described previously. + +**Omnibus Installation** + +``` +sudo gitlab-rake gitlab:repo:check +``` + +**Source Installation** + +```bash +sudo -u git -H bundle exec rake gitlab:repo:check RAILS_ENV=production +``` + +### Check repositories for a specific user + +This task checks all repositories that a specific user has access to. This is important +because sometimes you know which user is experiencing trouble but you don't know +which project might be the cause. + +If the rake task is executed without brackets at the end, you will be prompted +to enter a username. + +**Omnibus Installation** + +```bash +sudo gitlab-rake gitlab:user:check_repos +sudo gitlab-rake gitlab:user:check_repos[<username>] +``` + +**Source Installation** + +```bash +sudo -u git -H bundle exec rake gitlab:user:check_repos RAILS_ENV=production +sudo -u git -H bundle exec rake gitlab:user:check_repos[<username>] RAILS_ENV=production +``` + +Example output: + +![gitlab:user:check_repos output](../img/raketasks/check_repos_output.png) + +## LDAP Check + +The LDAP check Rake task will test the bind_dn and password credentials +(if configured) and will list a sample of LDAP users. This task is also +executed as part of the `gitlab:check` task, but can run independently +using the command below. + +**Omnibus Installation** + +``` +sudo gitlab-rake gitlab:ldap:check +``` + +**Source Installation** + +```bash +sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production +``` + +By default, the task will return a sample of 100 LDAP users. Change this +limit by passing a number to the check task: + +```bash +rake gitlab:ldap:check[50] +``` diff --git a/doc/api/groups.md b/doc/api/groups.md index b56d74d25e0..45a3118f27a 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -26,6 +26,15 @@ GET /groups You can search for groups by name or path, see below. +======= +## List owned groups + +Get a list of groups which are owned by the authenticated user. + +``` +GET /groups/owned +``` + ## List a group's projects Get a list of projects in this group. diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md index ff6c9e4931c..aea1c12a392 100644 --- a/doc/api/notification_settings.md +++ b/doc/api/notification_settings.md @@ -4,7 +4,7 @@ **Valid notification levels** -The notification levels are defined in the `NotificationSetting::level` model enumeration. Currently, these levels are recognized: +The notification levels are defined in the `NotificationSetting.level` model enumeration. Currently, these levels are recognized: ``` disabled @@ -28,6 +28,8 @@ reopen_merge_request close_merge_request reassign_merge_request merge_merge_request +failed_pipeline +success_pipeline ``` ## Global notification settings @@ -77,6 +79,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab | `close_merge_request` | boolean | no | Enable/disable this notification | | `reassign_merge_request` | boolean | no | Enable/disable this notification | | `merge_merge_request` | boolean | no | Enable/disable this notification | +| `failed_pipeline` | boolean | no | Enable/disable this notification | +| `success_pipeline` | boolean | no | Enable/disable this notification | Example response: @@ -141,6 +145,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab | `close_merge_request` | boolean | no | Enable/disable this notification | | `reassign_merge_request` | boolean | no | Enable/disable this notification | | `merge_merge_request` | boolean | no | Enable/disable this notification | +| `failed_pipeline` | boolean | no | Enable/disable this notification | +| `success_pipeline` | boolean | no | Enable/disable this notification | Example responses: @@ -161,7 +167,9 @@ Example responses: "reopen_merge_request": false, "close_merge_request": false, "reassign_merge_request": false, - "merge_merge_request": false + "merge_merge_request": false, + "failed_pipeline": false, + "success_pipeline": false } } ``` diff --git a/doc/api/users.md b/doc/api/users.md index a50ba5432fe..041df07c051 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -33,6 +33,18 @@ GET /users ] ``` +In addition, you can filter users based on states eg. `blocked`, `active` +This works only to filter users who are `blocked` or `active`. +It does not support `active=false` or `blocked=false`. + +``` +GET /users?active=true +``` + +``` +GET /users?blocked=true +``` + ### For admins ``` @@ -120,6 +132,8 @@ For example: GET /users?username=jack_smith ``` +You can search for users who are external with: `/users?external=true` + ## Single user Get a single user. diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md index 105e2f1242a..b8669964c84 100644 --- a/doc/development/instrumentation.md +++ b/doc/development/instrumentation.md @@ -24,7 +24,7 @@ namespace you can use the `configure` class method. This method simply yields the supplied block while passing `Gitlab::Metrics::Instrumentation` as its argument. An example: -``` +```ruby Gitlab::Metrics::Instrumentation.configure do |conf| conf.instrument_method(Foo, :bar) conf.instrument_method(Foo, :baz) @@ -41,7 +41,7 @@ Method instrumentation should be added in the initializer Instrumenting a single method: -``` +```ruby Gitlab::Metrics::Instrumentation.configure do |conf| conf.instrument_method(User, :find_by) end @@ -49,7 +49,7 @@ end Instrumenting an entire class hierarchy: -``` +```ruby Gitlab::Metrics::Instrumentation.configure do |conf| conf.instrument_class_hierarchy(ActiveRecord::Base) end @@ -57,7 +57,7 @@ end Instrumenting all public class methods: -``` +```ruby Gitlab::Metrics::Instrumentation.configure do |conf| conf.instrument_methods(User) end @@ -68,7 +68,7 @@ end The easiest way to check if a method has been instrumented is to check its source location. For example: -``` +```ruby method = Rugged::TagCollection.instance_method(:[]) method.source_location diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 61b0fbc89c9..fd8335d251e 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -60,7 +60,7 @@ migration was tested. If you need to remove index, please add a condition like in following example: -``` +```ruby remove_index :namespaces, column: :name if index_exists?(:namespaces, :name) ``` @@ -75,7 +75,7 @@ need for downtime. To use this method you must disable transactions by calling the method `disable_ddl_transaction!` in the body of your migration class like so: -``` +```ruby class MyMigration < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! @@ -96,7 +96,7 @@ the `up` and `down` methods in your migration class. For example, to add the column `foo` to the `projects` table with a default value of `10` you'd write the following: -``` +```ruby class MyMigration < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! @@ -125,7 +125,7 @@ set the limit to 8-bytes. This will allow the column to hold a value up to Rails migration example: -``` +```ruby add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8) # or @@ -145,7 +145,7 @@ Please prefer Arel and plain SQL over usual ActiveRecord syntax. In case of usin Example with Arel: -``` +```ruby users = Arel::Table.new(:users) users.group(users[:user_id]).having(users[:id].count.gt(5)) @@ -154,7 +154,7 @@ users.group(users[:user_id]).having(users[:id].count.gt(5)) Example with plain SQL and `quote_string` helper: -``` +```ruby select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag| tag_name = quote_string(tag["name"]) duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag_name}'").map{|tag| tag["id"]} diff --git a/doc/development/shell_commands.md b/doc/development/shell_commands.md index 65cdd74bdb6..73893f9dd46 100644 --- a/doc/development/shell_commands.md +++ b/doc/development/shell_commands.md @@ -129,7 +129,7 @@ Various methods for opening and reading files in Ruby can be used to read the standard output of a process instead of a file. The following two commands do roughly the same: -``` +```ruby `touch /tmp/pawned-by-backticks` File.read('|touch /tmp/pawned-by-file-read') ``` @@ -142,7 +142,7 @@ attacker cannot control the start of the filename string you are opening. For instance, the following is sufficient to protect against accidentally starting a shell command with `|`: -``` +```ruby # we assume repo_path is not controlled by the attacker (user) path = File.join(repo_path, user_input) # path cannot start with '|' now. @@ -160,7 +160,7 @@ Path traversal is a security where the program (GitLab) tries to restrict user access to a certain directory on disk, but the user manages to open a file outside that directory by taking advantage of the `../` path notation. -``` +```ruby # Suppose the user gave us a path and they are trying to trick us user_input = '../other-repo.git/other-file' @@ -177,7 +177,7 @@ File.open(full_path) do # Oops! A good way to protect against this is to compare the full path with its 'absolute path' according to Ruby's `File.absolute_path`. -``` +```ruby full_path = File.join(repo_path, user_input) if full_path != File.absolute_path(full_path) raise "Invalid path: #{full_path.inspect}" diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md index 5210ce0de9a..eb9bbb67e7d 100644 --- a/doc/integration/shibboleth.md +++ b/doc/integration/shibboleth.md @@ -10,7 +10,7 @@ To enable the Shibboleth OmniAuth provider you must: 1. Configure Apache shibboleth module. Installation and configuration of module it self is out of scope of this document. Check https://wiki.shibboleth.net/ for more info. -1. You can find Apache config in gitlab-recipes (https://github.com/gitlabhq/gitlab-recipes/blob/master/web-server/apache/gitlab-ssl.conf) +1. You can find Apache config in gitlab-recipes (https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache) Following changes are needed to enable shibboleth: diff --git a/doc/raketasks/check.md b/doc/raketasks/check.md index 3ff3fee6a40..f7f6a40cd04 100644 --- a/doc/raketasks/check.md +++ b/doc/raketasks/check.md @@ -1,63 +1,3 @@ # Check Rake Tasks -## Repository Integrity - -Even though Git is very resilient and tries to prevent data integrity issues, -there are times when things go wrong. The following Rake tasks intend to -help GitLab administrators diagnose problem repositories so they can be fixed. - -There are 3 things that are checked to determine integrity. - -1. Git repository file system check ([git fsck](https://git-scm.com/docs/git-fsck)). - This step verifies the connectivity and validity of objects in the repository. -1. Check for `config.lock` in the repository directory. -1. Check for any branch/references lock files in `refs/heads`. - -It's important to note that the existence of `config.lock` or reference locks -alone do not necessarily indicate a problem. Lock files are routinely created -and removed as Git and GitLab perform operations on the repository. They serve -to prevent data integrity issues. However, if a Git operation is interrupted these -locks may not be cleaned up properly. - -The following symptoms may indicate a problem with repository integrity. If users -experience these symptoms you may use the rake tasks described below to determine -exactly which repositories are causing the trouble. - -- Receiving an error when trying to push code - `remote: error: cannot lock ref` -- A 500 error when viewing the GitLab dashboard or when accessing a specific project. - -### Check all GitLab repositories - -This task loops through all repositories on the GitLab server and runs the -3 integrity checks described previously. - -``` -# omnibus-gitlab -sudo gitlab-rake gitlab:repo:check - -# installation from source -bundle exec rake gitlab:repo:check RAILS_ENV=production -``` - -### Check repositories for a specific user - -This task checks all repositories that a specific user has access to. This is important -because sometimes you know which user is experiencing trouble but you don't know -which project might be the cause. - -If the rake task is executed without brackets at the end, you will be prompted -to enter a username. - -```bash -# omnibus-gitlab -sudo gitlab-rake gitlab:user:check_repos -sudo gitlab-rake gitlab:user:check_repos[<username>] - -# installation from source -bundle exec rake gitlab:user:check_repos RAILS_ENV=production -bundle exec rake gitlab:user:check_repos[<username>] RAILS_ENV=production -``` - -Example output: - -![gitlab:user:check_repos output](check_repos_output.png) +This document was moved to [administration/raketasks/check](../administration/raketasks/check.md). diff --git a/doc/university/README.md b/doc/university/README.md index 49714e4fb59..4569bc72797 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -212,5 +212,8 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project 1. [Support Path](support/README.md) 1. [Sales Path (redirect to sales handbook)](https://about.gitlab.com/handbook/sales-onboarding/) +1. [User Training](training/user_training.md) +1. [GitLab Flow Training](training/gitlab_flow.md) +1. [Training Topics](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/university/training/topics/) 1. [GitLab architecture for noobs](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/development/architecture.md) 1. [Client Assessment of GitLab versus GitHub](https://docs.google.com/a/gitlab.com/spreadsheets/d/18cRF9Y5I6I7Z_ab6qhBEW55YpEMyU4PitZYjomVHM-M/edit?usp=sharing) diff --git a/doc/university/training/gitlab_flow.md b/doc/university/training/gitlab_flow.md new file mode 100755 index 00000000000..a7db1f2e069 --- /dev/null +++ b/doc/university/training/gitlab_flow.md @@ -0,0 +1,53 @@ +# GitLab Flow + +- A simplified branching strategy +- All features and fixes first go to master +- Allows for 'production' or 'stable' branches +- Bug fixes/hot fix patches are cherry-picked from master + +--- + +# Feature branches + +- Create a feature/bugfix branch to do all work +- Use merge requests to merge to master + +![inline](gitlab_flow/feature_branches.png) + +--- + +# Production branch + +- One, long-running production release branch + as opposed to individual stable branches +- Consider creating a tag for each version that gets deployed + +--- + +# Production branch + +![inline](gitlab_flow/production_branch.png) + +--- + +# Release branch + +- Useful if you release software to customers +- When preparing a new release, create stable branch + from master +- Consider creating a tag for each version +- Cherry-pick critical bug fixes to stable branch for patch release +- Never commit bug fixes directly to stable branch + +--- + +# Release branch + +![inline](gitlab_flow/release_branches.png) + +--- + +# More details + +Blog post on 'GitLab Flow' at +[http://doc.gitlab.com/ee/workflow/gitlab_flow.html](http://doc.gitlab.com/ee/workflow/gitlab_flow.html) diff --git a/doc/university/training/gitlab_flow/feature_branches.png b/doc/university/training/gitlab_flow/feature_branches.png Binary files differnew file mode 100644 index 00000000000..88addb623ee --- /dev/null +++ b/doc/university/training/gitlab_flow/feature_branches.png diff --git a/doc/university/training/gitlab_flow/production_branch.png b/doc/university/training/gitlab_flow/production_branch.png Binary files differnew file mode 100644 index 00000000000..33fb26dd621 --- /dev/null +++ b/doc/university/training/gitlab_flow/production_branch.png diff --git a/doc/university/training/gitlab_flow/release_branches.png b/doc/university/training/gitlab_flow/release_branches.png Binary files differnew file mode 100644 index 00000000000..da7ae53413a --- /dev/null +++ b/doc/university/training/gitlab_flow/release_branches.png diff --git a/doc/university/training/index.md b/doc/university/training/index.md new file mode 100755 index 00000000000..03179ff5a77 --- /dev/null +++ b/doc/university/training/index.md @@ -0,0 +1,6 @@ +# GitLab Training Material + +All GitLab training material is stored in markdown format. Slides are +generated using [Deskset](http://www.decksetapp.com/). + +All training material is open to public contribution. diff --git a/doc/university/training/logo.png b/doc/university/training/logo.png Binary files differnew file mode 100644 index 00000000000..cc831790405 --- /dev/null +++ b/doc/university/training/logo.png diff --git a/doc/university/training/topics/additional_resources.md b/doc/university/training/topics/additional_resources.md new file mode 100755 index 00000000000..1ee615432aa --- /dev/null +++ b/doc/university/training/topics/additional_resources.md @@ -0,0 +1,8 @@ +## Additional Resources + +1. GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/) +2. GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis) +3. Pro git book [http://git-scm.com/book](http://git-scm.com/book) +4. Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/) +5. Code School tutorial [http://try.github.io/](http://try.github.io/) +6. Contact Us - [subscribers@gitlab.com](subscribers@gitlab.com) diff --git a/doc/university/training/topics/agile_git.md b/doc/university/training/topics/agile_git.md new file mode 100755 index 00000000000..e6e4fea9b51 --- /dev/null +++ b/doc/university/training/topics/agile_git.md @@ -0,0 +1,33 @@ +# Agile and Git + +---------- + +## Agile + +Lean software development methods focused on collaboration and interaction +with fast and smaller deployment cycles. + +---------- + +## Where Git comes in + +Git is an excellent tool for an Agile team considering that it allows +decentralized and simultaneous development. + +---------- + +### Branching And Workflows + +Branching in an Agile environment usually happens around user stories with one +or more developers working on it. + +If more than one developer then another branch for each developer is also used +with his/her initials, and US id. + +After its tested merge into master and remove the branch. + +---------- + +## What about GitLab +Tools like GitLab enhance collaboration by adding dialog around code mainly +through issues and merge requests. diff --git a/doc/university/training/topics/bisect.md b/doc/university/training/topics/bisect.md new file mode 100755 index 00000000000..a60c4365e0c --- /dev/null +++ b/doc/university/training/topics/bisect.md @@ -0,0 +1,81 @@ +# Bisect + +---------- + +## Bisect + +- Find a commit that introduced a bug +- Works through a process of elimination +- Specify a known good and bad revision to begin + +---------- + +## Bisect + +1. Start the bisect process +2. Enter the bad revision (usually latest commit) +3. Enter a known good revision (commit/branch) +4. Run code to see if bug still exists +5. Tell bisect the result +6. Repeat the previous 2 items until you find the offending commit + +---------- + +## Setup + +``` + mkdir bisect-ex + cd bisect-ex + touch index.html + git add -A + git commit -m "starting out" + vi index.html + # Add all good + git add -A + git commit -m "second commit" + vi index.html + # Add all good 2 + git add -A + git commit -m "third commit" + vi index.html +``` + +---------- + +``` + # Add all good 3 + git add -A + git commit -m "fourth commit" + vi index.html + # This looks bad + git add -A + git commit -m "fifth commit" + vi index.html + # Really bad + git add -A + git commit -m "sixth commit" + vi index.html + # again just bad + git add -A + git commit -m "seventh commit" +``` + +---------- + +## Commands + +``` + git bisect start + # Test your code + git bisect bad + git bisect next + # Say yes to the warning + # Test + git bisect good + # Test + git bisect bad + # Test + git bisect good + # done + git bisect reset +``` diff --git a/doc/university/training/topics/cherry_picking.md b/doc/university/training/topics/cherry_picking.md new file mode 100755 index 00000000000..af7a70a2818 --- /dev/null +++ b/doc/university/training/topics/cherry_picking.md @@ -0,0 +1,39 @@ +# Cherry Pick + +---------- + +## Cherry Pick + +- Given an existing commit on one branch, apply the change to another branch +- Useful for backporting bug fixes to previous release branches +- Make the commit on the master branch and pick in to stable + +---------- + +## Cherry Pick + +1. Check out a new 'stable' branch from 'master' +1. Change back to 'master' +1. Edit '`cherry_pick.rb`' and commit the changes. +1. Check commit log to get the commit SHA +1. Check out the 'stable' branch +1. Cherry pick the commit using the SHA obtained earlier + +---------- + +## Commands + +```bash +git checkout master +git checkout -b stable +git checkout master + +# Edit `cherry_pick.rb` +git add cherry_pick.rb +git commit -m 'Fix bugs in cherry_pick.rb' +git log +# Copy commit SHA +git checkout stable + +git cherry-pick <commit SHA> +``` diff --git a/doc/university/training/topics/env_setup.md b/doc/university/training/topics/env_setup.md new file mode 100755 index 00000000000..8149379b36f --- /dev/null +++ b/doc/university/training/topics/env_setup.md @@ -0,0 +1,60 @@ +# Configure your environment + +---------- +## Install + +- **Windows** + - Install 'Git for Windows' from https://git-for-windows.github.io + +- **Mac** + - Type '`git`' in the Terminal application. + - If it's not installed, it will prompt you to install it. + +- **Linux** + ```bash + sudo yum install git-all + ``` + ```bash + sudo apt-get install git-all + ``` + +---------- + +## Configure Git + +One-time configuration of the Git client + +```bash +git config --global user.name "Your Name" +git config --global user.email you@example.com +``` + +---------- + +## Configure SSH Key + +```bash +ssh-keygen -t rsa -b 4096 -C "you@computer-name" +``` + +```bash +# You will be prompted for the following information. Press enter to accept the defaults. Defaults appear in parentheses. +Generating public/private rsa key pair. +Enter file in which to save the key (/Users/you/.ssh/id_rsa): +Enter passphrase (empty for no passphrase): +Enter same passphrase again: +Your identification has been saved in /Users/you/.ssh/id_rsa. +Your public key has been saved in /Users/you/.ssh/id_rsa.pub. +The key fingerprint is: +39:fc:ce:94:f4:09:13:95:64:9a:65:c1:de:05:4d:01 you@computer-name +``` + +Copy your public key and add it to your GitLab profile + +```bash +cat ~/.ssh/id_rsa.pub +``` + +```bash +ssh-rsa AAAAB3NzaC1yc2EAAAADAQEL17Ufacg8cDhlQMS5NhV8z3GHZdhCrZbl4gz you@example.com +``` diff --git a/doc/university/training/topics/explore_gitlab.md b/doc/university/training/topics/explore_gitlab.md new file mode 100755 index 00000000000..b65457728c0 --- /dev/null +++ b/doc/university/training/topics/explore_gitlab.md @@ -0,0 +1,10 @@ +# Explore GitLab projects + +---------- + +- Dashboard +- User Preferences +- Issues +- Milestones and Labels +- Manage project members +- Project settings diff --git a/doc/university/training/topics/feature_branching.md b/doc/university/training/topics/feature_branching.md new file mode 100755 index 00000000000..4b34406ea75 --- /dev/null +++ b/doc/university/training/topics/feature_branching.md @@ -0,0 +1,32 @@ +# Feature branching + +---------- + +- Efficient parallel workflow for teams +- Develop each feature in a branch +- Keeps changes isolated +- Consider a 1-to-1 link to issues +- Push branches to the server frequently + - Hint: This is a cheap backup for your work-in-progress code + +---------- + +## Feature branching + +1. Create a new feature branch called 'squash_some_bugs' +1. Edit '`bugs.rb`' and remove all the bugs. +1. Commit +1. Push + +---------- + +## Commands + +``` +git checkout -b squash_some_bugs +# Edit `bugs.rb` +git status +git add bugs.rb +git commit -m 'Fix some buggy code' +git push origin squash_some_bugs +``` diff --git a/doc/university/training/topics/getting_started.md b/doc/university/training/topics/getting_started.md new file mode 100755 index 00000000000..ec7bb2631aa --- /dev/null +++ b/doc/university/training/topics/getting_started.md @@ -0,0 +1,95 @@ +# Getting Started + +---------- + +## Instantiating Repositories + +* Create a new repository by instantiating it through +```bash +git init +``` +* Copy an existing project by cloning the repository through +```bash +git clone <url> +``` + +---------- + +## Central Repos + +* To instantiate a central repository a `--bare` flag is required. +* Bare repositories don't allow file editing or committing changes. +* Create a bare repo with +```bash +git init --bare project-name.git +``` + +---------- + +## Instantiate workflow with clone + +1. Create a project in your user namespace + - Choose to import from 'Any Repo by URL' and use + https://gitlab.com/gitlab-org/training-examples.git +2. Create a '`Workspace`' directory in your home directory. +3. Clone the '`training-examples`' project + +---------- + +## Commands + +``` +mkdir ~/workspace +cd ~/workspace + +git clone git@gitlab.example.com:<username>/training-examples.git +cd training-examples +``` +---------- + +## Git concepts + +**Untracked files** + +New files that Git has not been told to track previously. + +**Working area** + +Files that have been modified but are not committed. + +**Staging area** + +Modified files that have been marked to go in the next commit. + +---------- + +## Committing Workflow + +1. Edit '`edit_this_file.rb`' in '`training-examples`' +1. See it listed as a changed file (working area) +1. View the differences +1. Stage the file +1. Commit +1. Push the commit to the remote +1. View the git log + +---------- + +## Commands + +``` +# Edit `edit_this_file.rb` +git status +git diff +git add <file> +git commit -m 'My change' +git push origin master +git log +``` + +---------- + +## Note + +* git fetch vs pull +* Pull is git fetch + git merge diff --git a/doc/university/training/topics/git_add.md b/doc/university/training/topics/git_add.md new file mode 100755 index 00000000000..9ffb4b9c859 --- /dev/null +++ b/doc/university/training/topics/git_add.md @@ -0,0 +1,33 @@ +# Git Add + +---------- + +## Git Add + +Adds content to the index or staging area. + +* Adds a list of file +```bash +git add <files> +``` +* Adds all files including deleted ones +```bash +git add -A +``` + +---------- + +## Git add continued + +* Add all text files in current dir +```bash +git add *.txt +``` +* Add all text file in the project +```bash +git add "*.txt*" +``` +* Adds all files in directory +```bash +git add views/layouts/ +``` diff --git a/doc/university/training/topics/git_intro.md b/doc/university/training/topics/git_intro.md new file mode 100755 index 00000000000..ca1ff29d93b --- /dev/null +++ b/doc/university/training/topics/git_intro.md @@ -0,0 +1,24 @@ +# Git introduction + +---------- + +## Intro + +https://git-scm.com/about + +- Distributed version control + - Does not rely on connection to a central server + - Many copies of the complete history +- Powerful branching and merging +- Adapts to nearly any workflow +- Fast, reliable and stable file format + +---------- + +## Help! + +Use the tools at your disposal when you get stuck. + +- Use '`git help <command>`' command +- Use Google +- Read documentation at https://git-scm.com diff --git a/doc/university/training/topics/git_log.md b/doc/university/training/topics/git_log.md new file mode 100755 index 00000000000..32ebceff491 --- /dev/null +++ b/doc/university/training/topics/git_log.md @@ -0,0 +1,57 @@ +# Git Log + +---------- + +Git log lists commit history. It allows searching and filtering. + +* Initiate log +``` +git log +``` + +* Retrieve set number of records: +``` +git log -n 2 +``` + +* Search commits by author. Allows user name or a regular expression. +``` +git log --author="user_name" +``` + +---------- + +* Search by comment message. +``` +git log --grep="<pattern>" +``` + +* Search by date +``` +git log --since=1.month.ago --until=3.weeks.ago +``` + + +---------- + +## Git Log Workflow + +1. Change to workspace directory +2. Clone the multi runner projects +3. Change to project dir +4. Search by author +5. Search by date +6. Combine + +---------- + +## Commands + +``` +cd ~/workspace +git clone git@gitlab.com:gitlab-org/gitlab-ci-multi-runner.git +cd gitlab-ci-multi-runner +git log --author="Travis" +git log --since=1.month.ago --until=3.weeks.ago +git log --since=1.month.ago --until=1.day.ago --author="Travis" +``` diff --git a/doc/university/training/topics/gitlab_flow.md b/doc/university/training/topics/gitlab_flow.md new file mode 100755 index 00000000000..8e5d3baf959 --- /dev/null +++ b/doc/university/training/topics/gitlab_flow.md @@ -0,0 +1,53 @@ +# GitLab Flow + +---------- + +- A simplified branching strategy +- All features and fixes first go to master +- Allows for 'production' or 'stable' branches +- Bug fixes/hot fix patches are cherry-picked from master + +---------- + +### Feature branches + +- Create a feature/bugfix branch to do all work +- Use merge requests to merge to master + +![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/feature_branches.png) + +---------- + +## Production branch + +- One, long-running production release branch + as opposed to individual stable branches +- Consider creating a tag for each version that gets deployed + +---------- + +## Production branch + +![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/production_branch.png) + +---------- + +## Release branch + +- Useful if you release software to customers +- When preparing a new release, create stable branch + from master +- Consider creating a tag for each version +- Cherry-pick critical bug fixes to stable branch for patch release +- Never commit bug fixes directly to stable branch + +---------- + +![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/release_branches.png) + +---------- + +## More details + +Blog post on 'GitLab Flow' at +[http://doc.gitlab.com/ee/workflow/gitlab_flow.html](http://doc.gitlab.com/ee/workflow/gitlab_flow.html) diff --git a/doc/university/training/topics/merge_conflicts.md b/doc/university/training/topics/merge_conflicts.md new file mode 100755 index 00000000000..77807b3e7ef --- /dev/null +++ b/doc/university/training/topics/merge_conflicts.md @@ -0,0 +1,70 @@ +# Merge conflicts + +---------- + +- Happen often +- Learning to fix conflicts is hard +- Practice makes perfect +- Force push after fixing conflicts. Be careful! + +---------- + +## Merge conflicts + +1. Checkout a new branch and edit `conflicts.rb`. Add 'Line4' and 'Line5'. +2. Commit and push +3. Checkout master and edit `conflicts.rb`. Add 'Line6' and 'Line7' below 'Line3'. +4. Commit and push to master +5. Create a merge request and watch it fail +6. Rebase our new branch with master +7. Fix conflicts on the `conflicts.rb` file. +8. Stage the file and continue rebasing +9. Force push the changes +10. Finally continue with the Merge Request + +---------- + +## Commands + +``` +git checkout -b conflicts_branch + +# vi conflicts.rb +# Add 'Line4' and 'Line5' + +git commit -am "add line4 and line5" +git push origin conflicts_branch + +git checkout master + +# vi conflicts.rb +# Add 'Line6' and 'Line7' +git commit -am "add line6 and line7" +git push origin master +``` + +Create a merge request on the GitLab web UI. You'll see a conflict warning. + +``` +git checkout conflicts_branch +git fetch +git rebase master + +# Fix conflicts by editing the files. + +git add conflicts.rb +# No need to commit this file + +git rebase --continue + +# Remember that we have rewritten our commit history so we +# need to force push so that our remote branch is restructured +git push origin conflicts_branch -f +``` +---------- + +## Note +* When to use 'git merge' and when to use 'git rebase' +* Rebase when updating your branch with master +* Merge when bringing changes from feature to master +* Reference: https://www.atlassian.com/git/tutorials/merging-vs-rebasing/ diff --git a/doc/university/training/topics/merge_requests.md b/doc/university/training/topics/merge_requests.md new file mode 100755 index 00000000000..5b446f02f63 --- /dev/null +++ b/doc/university/training/topics/merge_requests.md @@ -0,0 +1,43 @@ +# Merge requests + +---------- + +- When you want feedback create a merge request +- Target is the default branch (usually master) +- Assign or mention the person you would like to review +- Add 'WIP' to the title if it's a work in progress +- When accepting, always delete the branch +- Anyone can comment, not just the assignee +- Push corrections to the same branch + +---------- + +## Merge requests + +**Create your first merge request** + +1. Use the blue button in the activity feed +1. View the diff (changes) and leave a comment +1. Push a new commit to the same branch +1. Review the changes again and notice the update + +---------- + +## Feedback and Collaboration + +- Merge requests are a time for feedback and collaboration +- Giving feedback is hard +- Be as kind as possible +- Receiving feedback is hard +- Be as receptive as possible +- Feedback is about the best code, not the person. You are not your code + +---------- + +## Feedback and Collaboration + +Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests: +[https://github.com/thoughtbot/guides/tree/master/code-review](https://github.com/thoughtbot/guides/tree/master/code-review) + +See GitLab merge requests for examples: +[https://gitlab.com/gitlab-org/gitlab-ce/merge_requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests) diff --git a/doc/university/training/topics/rollback_commits.md b/doc/university/training/topics/rollback_commits.md new file mode 100755 index 00000000000..cf647284604 --- /dev/null +++ b/doc/university/training/topics/rollback_commits.md @@ -0,0 +1,81 @@ +# Rollback Commits + +---------- + +## Undo Commits + +* Undo last commit putting everything back into the staging area. +``` +git reset --soft HEAD^ +``` + +* Add files and change message with: +``` +git commit --amend -m "New Message" +``` + +---------- + +* Undo last and remove changes +``` +git reset --hard HEAD^ +``` + +* Same as last one but for two commits back +``` +git reset --hard HEAD^^ +``` + +** Don't reset after pushing ** + +---------- + +## Reset Workflow + +1. Edit file again 'edit_this_file.rb' +2. Check status +3. Add and commit with wrong message +4. Check log +5. Amend commit +6. Check log +7. Soft reset +8. Check log +9. Pull for updates +10. Push changes + + +---------- + +## Commands + +``` +# Change file edit_this_file.rb +git status +git commit -am "kjkfjkg" +git log +git commit --amend -m "New comment added" +git log +git reset --soft HEAD^ +git log +git pull origin master +git push origin master +``` + +---------- + +## Note + +* git revert vs git reset +* Reset removes the commit while revert removes the changes but leaves the commit +* Revert is safer considering we can revert a revert + +``` +# Changed file +git commit -am "bug introduced" +git revert HEAD +# New commit created reverting changes +# Now we want to re apply the reverted commit +git log # take hash from the revert commit +git revert <rev commit hash> +# reverted commit is back (new commit created again) +``` diff --git a/doc/university/training/topics/stash.md b/doc/university/training/topics/stash.md new file mode 100755 index 00000000000..c1bdda32645 --- /dev/null +++ b/doc/university/training/topics/stash.md @@ -0,0 +1,86 @@ +# Git Stash + +---------- + +We use git stash to store our changes when they are not ready to be committed +and we need to change to a different branch. + +* Stash +``` +git stash save +# or +git stash +# or with a message +git stash save "this is a message to display on the list" +``` + +* Apply stash to keep working on it +``` +git stash apply +# or apply a specific one from out stack +git stash apply stash@{3} +``` + +---------- + +* Every time we save a stash it gets stacked so by using list we can see all our +stashes. + +``` +git stash list +# or for more information (log methods) +git stash list --stat +``` + +* To clean our stack we need to manually remove them. + +``` +# drop top stash +git stash drop +# or +git stash drop <name> +# to clear all history we can use +git stash clear +``` + +---------- + +* Apply and drop on one command + +``` + git stash pop +``` + +* If we meet conflicts we need to either reset or commit our changes. + +* Conflicts through `pop` will not drop a stash afterwards. + +---------- + +## Git Stash + +1. Modify a file +2. Stage file +3. Stash it +4. View our stash list +5. Confirm no pending changes through status +5. Apply with pop +6. View list to confirm changes + +---------- + +## Commands + +``` +# Modify edit_this_file.rb file +git add . + +git stash save "Saving changes from edit this file" + +git stash list +git status + +git stash pop +git stash list +git status +``` diff --git a/doc/university/training/topics/subtree.md b/doc/university/training/topics/subtree.md new file mode 100755 index 00000000000..5d869af64c1 --- /dev/null +++ b/doc/university/training/topics/subtree.md @@ -0,0 +1,55 @@ +## Subtree + +---------- + +## Subtree + +* Used when there are nested repositories. +* Not recommended when the amount of dependencies is too large +* For these cases we need a dependency control system +* Command are painfully long so aliases are necessary + +---------- + +## Subtree Aliases + +* Add: git subtree add --prefix <target-folder> <url> <branch> --squash +* Pull: git subtree add --prefix <target-folder> <url> <branch> --squash +* Push: git subtree add --prefix <target-folder> <url> <branch> +* Ex: git config alias.sbp 'subtree pull --prefix st / + git@gitlab.com:balameb/subtree-nested-example.git master --squash' + +---------- + +``` + # Add an alias + # Add + git config alias.sba 'subtree add --prefix st / + git@gitlab.com:balameb/subtree-nested-example.git master --squash' + # Pull + git config alias.sbpl 'subtree pull --prefix st / + git@gitlab.com:balameb/subtree-nested-example.git master --squash' + # Push + git config alias.sbph 'subtree push --prefix st / + git@gitlab.com:balameb/subtree-nested-example.git master' + + # Adding this subtree adds a st dir with a readme + git sba + vi st/README.md + # Edit file + git status shows differences + +``` + +---------- + +``` + # Adding, or committing won't change the sub repo at remote + # even if we push + git add -A + git commit -m "Adding to subtree readme" + + # Push to subtree repo + git sbph + # now we can check our remote sub repo +``` diff --git a/doc/university/training/topics/tags.md b/doc/university/training/topics/tags.md new file mode 100755 index 00000000000..e9607b5a875 --- /dev/null +++ b/doc/university/training/topics/tags.md @@ -0,0 +1,38 @@ +# Tags + +---------- + +- Useful for marking deployments and releases +- Annotated tags are an unchangeable part of Git history +- Soft/lightweight tags can be set and removed at will +- Many projects combine an anotated release tag with a stable branch +- Consider setting deployment/release tags automatically + +---------- + +# Tags + +- Create a lightweight tag +- Create an annotated tag +- Push the tags to the remote repository + +**Additional resources** + +[http://git-scm.com/book/en/Git-Basics-Tagging](http://git-scm.com/book/en/Git-Basics-Tagging) + +---------- + +# Commands + +``` +git checkout master + +# Lightweight tag +git tag my_lightweight_tag + +# Annotated tag +git tag -a v1.0 -m ‘Version 1.0’ +git tag + +git push origin --tags +``` diff --git a/doc/university/training/topics/unstage.md b/doc/university/training/topics/unstage.md new file mode 100755 index 00000000000..17dbb64b9e6 --- /dev/null +++ b/doc/university/training/topics/unstage.md @@ -0,0 +1,31 @@ +# Unstage + +---------- + +## Unstage + +* To remove files from stage use reset HEAD. Where HEAD is the last commit of the current branch. + +```bash +git reset HEAD <file> +``` + +* This will unstage the file but maintain the modifications. To revert the file back to the state it was in before the changes we can use: + +```bash +git checkout -- <file> +``` + +---------- + +* To remove a file from disk and repo use 'git rm' and to rm a dir use the '-r' flag. +``` +git rm '*.txt' +git rm -r <dirname> +``` + + +* If we want to remove a file from the repository but keep it on disk, say we forgot to add it to our `.gitignore` file then use `--cache`. +``` +git rm <filename> --cache +``` diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md new file mode 100755 index 00000000000..35afe73708f --- /dev/null +++ b/doc/university/training/user_training.md @@ -0,0 +1,392 @@ +# GitLab Git Workshop + +--- + +# Agenda + +1. Brief history of Git +1. GitLab walkthrough +1. Configure your environment +1. Workshop + +--- + +# Git introduction + +https://git-scm.com/about + +- Distributed version control + - Does not rely on connection to a central server + - Many copies of the complete history +- Powerful branching and merging +- Adapts to nearly any workflow +- Fast, reliable and stable file format + +--- + +# Help! + +Use the tools at your disposal when you get stuck. + +- Use '`git help <command>`' command +- Use Google +- Read documentation at https://git-scm.com + +--- + +# GitLab Walkthrough + +![fit](logo.png) + +--- + +# Configure your environment + +- Windows: Install 'Git for Windows' + +> https://git-for-windows.github.io + +- Mac: Type '`git`' in the Terminal application. + +> If it's not installed, it will prompt you to install it. + +- Debian: '`sudo apt-get install git-all`' +or Red Hat '`sudo yum install git-all`' + +--- + +# Git Workshop + +## Overview + +1. Configure Git +1. Configure SSH Key +1. Create a project +1. Committing +1. Feature branching +1. Merge requests +1. Feedback and Collaboration + +--- + +# Configure Git + +One-time configuration of the Git client + +```bash +git config --global user.name "Your Name" +git config --global user.email you@example.com +``` + +--- + +# Configure SSH Key + +```bash +ssh-keygen -t rsa -b 4096 -C "you@computer-name" +``` + +```bash +# You will be prompted for the following information. Press enter to accept the defaults. Defaults appear in parentheses. +Generating public/private rsa key pair. +Enter file in which to save the key (/Users/you/.ssh/id_rsa): +Enter passphrase (empty for no passphrase): +Enter same passphrase again: +Your identification has been saved in /Users/you/.ssh/id_rsa. +Your public key has been saved in /Users/you/.ssh/id_rsa.pub. +The key fingerprint is: +39:fc:ce:94:f4:09:13:95:64:9a:65:c1:de:05:4d:01 you@computer-name +``` + +Copy your public key and add it to your GitLab profile + +```bash +cat ~/.ssh/id_rsa.pub +``` + +```bash +ssh-rsa AAAAB3NzaC1yc2EAAAADAQEL17Ufacg8cDhlQMS5NhV8z3GHZdhCrZbl4gz you@example.com +``` + +--- + +# Create a project + +- Create a project in your user namespace + - Choose to import from 'Any Repo by URL' and use + https://gitlab.com/gitlab-org/training-examples.git +- Create a '`development`' or '`workspace`' directory in your home directory. +- Clone the '`training-examples`' project + +--- + +# Commands + +``` +mkdir ~/development +cd ~/development + +-or- + +mkdir ~/workspace +cd ~/workspace + +git clone git@gitlab.example.com:<username>/training-examples.git +cd training-examples +``` + +--- + +# Git concepts + +**Untracked files** + +New files that Git has not been told to track previously. + +**Working area** + +Files that have been modified but are not committed. + +**Staging area** + +Modified files that have been marked to go in the next commit. + +--- + +# Committing + +1. Edit '`edit_this_file.rb`' in '`training-examples`' +1. See it listed as a changed file (working area) +1. View the differences +1. Stage the file +1. Commit +1. Push the commit to the remote +1. View the git log + +--- + +# Commands + +``` +# Edit `edit_this_file.rb` +git status +git diff +git add <file> +git commit -m 'My change' +git push origin master +git log +``` + +--- + +# Feature branching + +- Efficient parallel workflow for teams +- Develop each feature in a branch +- Keeps changes isolated +- Consider a 1-to-1 link to issues +- Push branches to the server frequently + - Hint: This is a cheap backup for your work-in-progress code + +--- + +# Feature branching + +1. Create a new feature branch called 'squash_some_bugs' +1. Edit '`bugs.rb`' and remove all the bugs. +1. Commit +1. Push + +--- + +# Commands + +``` +git checkout -b squash_some_bugs +# Edit `bugs.rb` +git status +git add bugs.rb +git commit -m 'Fix some buggy code' +git push origin squash_some_bugs +``` + +--- + +# Merge requests + +- When you want feedback create a merge request +- Target is the ‘default’ branch (usually master) +- Assign or mention the person you would like to review +- Add 'WIP' to the title if it's a work in progress +- When accepting, always delete the branch +- Anyone can comment, not just the assignee +- Push corrections to the same branch + +--- + +# Merge requests + +**Create your first merge request** + +1. Use the blue button in the activity feed +1. View the diff (changes) and leave a comment +1. Push a new commit to the same branch +1. Review the changes again and notice the update + +--- + +# Feedback and Collaboration + +- Merge requests are a time for feedback and collaboration +- Giving feedback is hard +- Be as kind as possible +- Receiving feedback is hard +- Be as receptive as possible +- Feedback is about the best code, not the person. You are not your code + +--- + +# Feedback and Collaboration + +Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests: +[https://github.com/thoughtbot/guides/tree/master/code-review](https://github.com/thoughtbot/guides/tree/master/code-review) + +See GitLab merge requests for examples: +[https://gitlab.com/gitlab-org/gitlab-ce/merge_requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests) + +--- + +# Explore GitLab projects + +![fit](logo.png) + +- Dashboard +- User Preferences +- ReadMe, Changelog, License shortcuts +- Issues +- Milestones and Labels +- Manage project members +- Project settings + +--- + +# Tags + +- Useful for marking deployments and releases +- Annotated tags are an unchangeable part of Git history +- Soft/lightweight tags can be set and removed at will +- Many projects combine an anotated release tag with a stable branch +- Consider setting deployment/release tags automatically + +--- + +# Tags + +- Create a lightweight tag +- Create an annotated tag +- Push the tags to the remote repository + +**Additional resources** + +[http://git-scm.com/book/en/Git-Basics-Tagging](http://git-scm.com/book/en/Git-Basics-Tagging) + +--- + +# Commands + +``` +git checkout master + +# Lightweight tag +git tag my_lightweight_tag + +# Annotated tag +git tag -a v1.0 -m ‘Version 1.0’ +git tag + +git push origin --tags +``` + +--- + +# Merge conflicts + +- Happen often +- Learning to fix conflicts is hard +- Practice makes perfect +- Force push after fixing conflicts. Be careful! + +--- + +# Merge conflicts + +1. Checkout a new branch and edit `conflicts.rb`. Add 'Line4' and 'Line5'. +1. Commit and push +1. Checkout master and edit `conflicts.rb`. Add 'Line6' and 'Line7' below 'Line3'. +1. Commit and push to master +1. Create a merge request + +--- + +# Merge conflicts + +After creating a merge request you should notice that conflicts exist. Resolve +the conflicts locally by rebasing. + +``` +git rebase master + +# Fix conflicts by editing the files. + +git add conflicts.rb +git commit -m 'Fix conflicts' +git rebase --continue +git push origin <branch> -f +``` + +--- + +# Rebase with squash + +You may end up with a commit log that looks like this: + +``` +Fix issue #13 +Test +Fix +Fix again +Test +Test again +Does this work? +``` + +Squash these in to meaningful commits using an interactive rebase. + +--- + +# Rebase with squash + +Squash the commits on the same branch we used for the merge conflicts step. + +``` +git rebase -i master +``` + +In the editor, leave the first commit as 'pick' and set others to 'fixup'. + +--- + +# Questions? + +![fit](logo.png) + +Thank you for your hard work! + +**Additional Resources** + +GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/) +GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis) +Pro git book [http://git-scm.com/book](http://git-scm.com/book) +Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/) +Code School tutorial [http://try.github.io/](http://try.github.io/) +Contact Us - [subscribers@gitlab.com](subscribers@gitlab.com) diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index 1b49a5c385f..c936e8833c6 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -66,6 +66,7 @@ Below is the table of events users can be notified of: In all of the below cases, the notification will be sent to: - Participants: - the author and assignee of the issue/merge request + - the author of the pipeline - authors of comments on the issue/merge request - anyone mentioned by `@username` in the issue/merge request title or description - anyone mentioned by `@username` in any of the comments on the issue/merge request @@ -88,6 +89,8 @@ In all of the below cases, the notification will be sent to: | Reopen merge request | | | Merge merge request | | | New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher | +| Failed pipeline | The above, plus the author of the pipeline | +| Successful pipeline | The above, plus the author of the pipeline | In addition, if the title or description of an Issue or Merge Request is diff --git a/lib/api/groups.rb b/lib/api/groups.rb index a13e353b7f5..40644fc2adf 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -26,6 +26,16 @@ module API present @groups, with: Entities::Group end + # Get list of owned groups for authenticated user + # + # Example Request: + # GET /groups/owned + get '/owned' do + @groups = current_user.owned_groups + @groups = paginate @groups + present @groups, with: Entities::Group, user: current_user + end + # Create group. Available only for users who can create groups. # # Parameters: diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index 9b73f6826cf..8984cf8cdcd 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -11,19 +11,25 @@ module API else milestones end end + + params :optional_params do + optional :description, type: String, desc: 'The description of the milestone' + optional :due_date, type: String, desc: 'The due date of the milestone' + end end + params do + requires :id, type: String, desc: 'The ID of a project' + end resource :projects do - # Get a list of project milestones - # - # Parameters: - # id (required) - The ID of a project - # state (optional) - Return "active" or "closed" milestones - # Example Request: - # GET /projects/:id/milestones - # GET /projects/:id/milestones?iid=42 - # GET /projects/:id/milestones?state=active - # GET /projects/:id/milestones?state=closed + desc 'Get a list of project milestones' do + success Entities::Milestone + end + params do + optional :state, type: String, values: %w[active closed all], default: 'all', + desc: 'Return "active", "closed", or "all" milestones' + optional :iid, type: Integer, desc: 'The IID of the milestone' + end get ":id/milestones" do authorize! :read_milestone, user_project @@ -34,34 +40,31 @@ module API present paginate(milestones), with: Entities::Milestone end - # Get a single project milestone - # - # Parameters: - # id (required) - The ID of a project - # milestone_id (required) - The ID of a project milestone - # Example Request: - # GET /projects/:id/milestones/:milestone_id + desc 'Get a single project milestone' do + success Entities::Milestone + end + params do + requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' + end get ":id/milestones/:milestone_id" do authorize! :read_milestone, user_project - @milestone = user_project.milestones.find(params[:milestone_id]) - present @milestone, with: Entities::Milestone + milestone = user_project.milestones.find(params[:milestone_id]) + present milestone, with: Entities::Milestone end - # Create a new project milestone - # - # Parameters: - # id (required) - The ID of the project - # title (required) - The title of the milestone - # description (optional) - The description of the milestone - # due_date (optional) - The due date of the milestone - # Example Request: - # POST /projects/:id/milestones + desc 'Create a new project milestone' do + success Entities::Milestone + end + params do + requires :title, type: String, desc: 'The title of the milestone' + use :optional_params + end post ":id/milestones" do authorize! :admin_milestone, user_project - required_attributes! [:title] - attrs = attributes_for_keys [:title, :description, :due_date] - milestone = ::Milestones::CreateService.new(user_project, current_user, attrs).execute + milestone_params = declared(params, include_parent_namespaces: false) + + milestone = ::Milestones::CreateService.new(user_project, current_user, milestone_params).execute if milestone.valid? present milestone, with: Entities::Milestone @@ -70,22 +73,23 @@ module API end end - # Update an existing project milestone - # - # Parameters: - # id (required) - The ID of a project - # milestone_id (required) - The ID of a project milestone - # title (optional) - The title of a milestone - # description (optional) - The description of a milestone - # due_date (optional) - The due date of a milestone - # state_event (optional) - The state event of the milestone (close|activate) - # Example Request: - # PUT /projects/:id/milestones/:milestone_id + desc 'Update an existing project milestone' do + success Entities::Milestone + end + params do + requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' + optional :title, type: String, desc: 'The title of the milestone' + optional :state_event, type: String, values: %w[close activate], + desc: 'The state event of the milestone ' + use :optional_params + at_least_one_of :title, :description, :due_date, :state_event + end put ":id/milestones/:milestone_id" do authorize! :admin_milestone, user_project - attrs = attributes_for_keys [:title, :description, :due_date, :state_event] - milestone = user_project.milestones.find(params[:milestone_id]) - milestone = ::Milestones::UpdateService.new(user_project, current_user, attrs).execute(milestone) + milestone_params = declared(params, include_parent_namespaces: false, include_missing: false) + + milestone = user_project.milestones.find(milestone_params.delete(:milestone_id)) + milestone = ::Milestones::UpdateService.new(user_project, current_user, milestone_params).execute(milestone) if milestone.valid? present milestone, with: Entities::Milestone @@ -94,21 +98,20 @@ module API end end - # Get all issues for a single project milestone - # - # Parameters: - # id (required) - The ID of a project - # milestone_id (required) - The ID of a project milestone - # Example Request: - # GET /projects/:id/milestones/:milestone_id/issues + desc 'Get all issues for a single project milestone' do + success Entities::Issue + end + params do + requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' + end get ":id/milestones/:milestone_id/issues" do authorize! :read_milestone, user_project - @milestone = user_project.milestones.find(params[:milestone_id]) + milestone = user_project.milestones.find(params[:milestone_id]) finder_params = { project_id: user_project.id, - milestone_title: @milestone.title + milestone_title: milestone.title } issues = IssuesFinder.new(current_user, finder_params).execute diff --git a/lib/api/runners.rb b/lib/api/runners.rb index ecc8f2fc5a2..84c19c432b0 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -1,34 +1,39 @@ module API - # Runners API class Runners < Grape::API before { authenticate! } resource :runners do - # Get runners available for user - # - # Example Request: - # GET /runners + desc 'Get runners available for user' do + success Entities::Runner + end + params do + optional :scope, type: String, values: %w[active paused online], + desc: 'The scope of specific runners to show' + end get do runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: ['specific', 'shared']) present paginate(runners), with: Entities::Runner end - # Get all runners - shared and specific - # - # Example Request: - # GET /runners/all + desc 'Get all runners - shared and specific' do + success Entities::Runner + end + params do + optional :scope, type: String, values: %w[active paused online specific shared], + desc: 'The scope of specific runners to show' + end get 'all' do authenticated_as_admin! runners = filter_runners(Ci::Runner.all, params[:scope]) present paginate(runners), with: Entities::Runner end - # Get runner's details - # - # Parameters: - # id (required) - The ID of ther runner - # Example Request: - # GET /runners/:id + desc "Get runner's details" do + success Entities::RunnerDetails + end + params do + requires :id, type: Integer, desc: 'The ID of the runner' + end get ':id' do runner = get_runner(params[:id]) authenticate_show_runner!(runner) @@ -36,33 +41,37 @@ module API present runner, with: Entities::RunnerDetails, current_user: current_user end - # Update runner's details - # - # Parameters: - # id (required) - The ID of ther runner - # description (optional) - Runner's description - # active (optional) - Runner's status - # tag_list (optional) - Array of tags for runner - # Example Request: - # PUT /runners/:id + desc "Update runner's details" do + success Entities::RunnerDetails + end + params do + requires :id, type: Integer, desc: 'The ID of the runner' + optional :description, type: String, desc: 'The description of the runner' + optional :active, type: Boolean, desc: 'The state of a runner' + optional :tag_list, type: Array[String], desc: 'The list of tags for a runner' + optional :run_untagged, type: Boolean, desc: 'Flag indicating the runner can execute untagged jobs' + optional :locked, type: Boolean, desc: 'Flag indicating the runner is locked' + at_least_one_of :description, :active, :tag_list, :run_untagged, :locked + end put ':id' do - runner = get_runner(params[:id]) + runner = get_runner(params.delete(:id)) authenticate_update_runner!(runner) - attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged, :locked] - if runner.update(attrs) + runner_params = declared(params, include_missing: false) + + if runner.update(runner_params) present runner, with: Entities::RunnerDetails, current_user: current_user else render_validation_error!(runner) end end - # Remove runner - # - # Parameters: - # id (required) - The ID of ther runner - # Example Request: - # DELETE /runners/:id + desc 'Remove a runner' do + success Entities::Runner + end + params do + requires :id, type: Integer, desc: 'The ID of the runner' + end delete ':id' do runner = get_runner(params[:id]) authenticate_delete_runner!(runner) @@ -72,28 +81,31 @@ module API end end + params do + requires :id, type: String, desc: 'The ID of a project' + end resource :projects do before { authorize_admin_project } - # Get runners available for project - # - # Example Request: - # GET /projects/:id/runners + desc 'Get runners available for project' do + success Entities::Runner + end + params do + optional :scope, type: String, values: %w[active paused online specific shared], + desc: 'The scope of specific runners to show' + end get ':id/runners' do runners = filter_runners(Ci::Runner.owned_or_shared(user_project.id), params[:scope]) present paginate(runners), with: Entities::Runner end - # Enable runner for project - # - # Parameters: - # id (required) - The ID of the project - # runner_id (required) - The ID of the runner - # Example Request: - # POST /projects/:id/runners/:runner_id + desc 'Enable a runner for a project' do + success Entities::Runner + end + params do + requires :runner_id, type: Integer, desc: 'The ID of the runner' + end post ':id/runners' do - required_attributes! [:runner_id] - runner = get_runner(params[:runner_id]) authenticate_enable_runner!(runner) @@ -106,13 +118,12 @@ module API end end - # Disable project's runner - # - # Parameters: - # id (required) - The ID of the project - # runner_id (required) - The ID of the runner - # Example Request: - # DELETE /projects/:id/runners/:runner_id + desc "Disable project's runner" do + success Entities::Runner + end + params do + requires :runner_id, type: Integer, desc: 'The ID of the runner' + end delete ':id/runners/:runner_id' do runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id]) not_found!('Runner') unless runner_project diff --git a/lib/api/session.rb b/lib/api/session.rb index 55ec66a6d67..d09400b81f5 100644 --- a/lib/api/session.rb +++ b/lib/api/session.rb @@ -1,15 +1,14 @@ module API - # Users API class Session < Grape::API - # Login to get token - # - # Parameters: - # login (*required) - user login - # email (*required) - user email - # password (required) - user password - # - # Example Request: - # POST /session + desc 'Login to get token' do + success Entities::UserLogin + end + params do + optional :login, type: String, desc: 'The username' + optional :email, type: String, desc: 'The email of the user' + requires :password, type: String, desc: 'The password of the user' + at_least_one_of :login, :email + end post "/session" do user = Gitlab::Auth.find_with_user_password(params[:email] || params[:login], params[:password]) diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index d1d07394e92..9a4f1cd342f 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -1,19 +1,18 @@ module API - # Triggers API class Triggers < Grape::API + params do + requires :id, type: String, desc: 'The ID of a project' + end resource :projects do - # Trigger a GitLab project build - # - # Parameters: - # id (required) - The ID of a CI project - # ref (required) - The name of project's branch or tag - # token (required) - The uniq token of trigger - # variables (optional) - The list of variables to be injected into build - # Example Request: - # POST /projects/:id/trigger/builds + desc 'Trigger a GitLab project build' do + success Entities::TriggerRequest + end + params do + requires :ref, type: String, desc: 'The commit sha or name of a branch or tag' + requires :token, type: String, desc: 'The unique token of trigger' + optional :variables, type: Hash, desc: 'The list of variables to be injected into build' + end post ":id/trigger/builds" do - required_attributes! [:ref, :token] - project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id]) trigger = Ci::Trigger.find_by_token(params[:token].to_s) not_found! unless project && trigger @@ -22,10 +21,6 @@ module API # validate variables variables = params[:variables] if variables - unless variables.is_a?(Hash) - render_api_error!('variables needs to be a hash', 400) - end - unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) } render_api_error!('variables needs to be a map of key-valued strings', 400) end @@ -44,31 +39,24 @@ module API end end - # Get triggers list - # - # Parameters: - # id (required) - The ID of a project - # page (optional) - The page number for pagination - # per_page (optional) - The value of items per page to show - # Example Request: - # GET /projects/:id/triggers + desc 'Get triggers list' do + success Entities::Trigger + end get ':id/triggers' do authenticate! authorize! :admin_build, user_project triggers = user_project.triggers.includes(:trigger_requests) - triggers = paginate(triggers) - present triggers, with: Entities::Trigger + present paginate(triggers), with: Entities::Trigger end - # Get specific trigger of a project - # - # Parameters: - # id (required) - The ID of a project - # token (required) - The `token` of a trigger - # Example Request: - # GET /projects/:id/triggers/:token + desc 'Get specific trigger of a project' do + success Entities::Trigger + end + params do + requires :token, type: String, desc: 'The unique token of trigger' + end get ':id/triggers/:token' do authenticate! authorize! :admin_build, user_project @@ -79,12 +67,9 @@ module API present trigger, with: Entities::Trigger end - # Create trigger - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # POST /projects/:id/triggers + desc 'Create a trigger' do + success Entities::Trigger + end post ':id/triggers' do authenticate! authorize! :admin_build, user_project @@ -94,13 +79,12 @@ module API present trigger, with: Entities::Trigger end - # Delete trigger - # - # Parameters: - # id (required) - The ID of a project - # token (required) - The `token` of a trigger - # Example Request: - # DELETE /projects/:id/triggers/:token + desc 'Delete a trigger' do + success Entities::Trigger + end + params do + requires :token, type: String, desc: 'The unique token of trigger' + end delete ':id/triggers/:token' do authenticate! authorize! :admin_build, user_project diff --git a/lib/api/users.rb b/lib/api/users.rb index c28e07a76b7..298c401a816 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -10,6 +10,9 @@ module API # GET /users # GET /users?search=Admin # GET /users?username=root + # GET /users?active=true + # GET /users?external=true + # GET /users?blocked=true get do unless can?(current_user, :read_users_list, nil) render_api_error!("Not authorized.", 403) @@ -19,8 +22,10 @@ module API @users = User.where(username: params[:username]) else @users = User.all - @users = @users.active if params[:active].present? + @users = @users.active if to_boolean(params[:active]) @users = @users.search(params[:search]) if params[:search].present? + @users = @users.blocked if to_boolean(params[:blocked]) + @users = @users.external if to_boolean(params[:external]) && current_user.is_admin? @users = paginate @users end diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb index 799b83b1069..80c844baecd 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 f5d110e987b..d8a855ec1fe 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 0fee9d267de..8c54a041cb8 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 69d01f8db15..0878b6afba3 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 a1264db2111..6e7b7669578 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 e5d1eb11d7f..aa76c64ac5f 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 c9a9ca79c09..40451947e6c 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 a000ac61e5c..d3968d6b229 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 fa71b3c952a..63b592137bb 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 863f5725d3b..7adaffa19c1 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/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index b164f5a2eea..7e3d5647b39 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/diff/file.rb b/lib/gitlab/diff/file.rb index ce85e5e0123..5110bfbf898 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -126,7 +126,7 @@ module Gitlab repository.blob_at(commit.id, file_path) end - def cache_key + def file_identifier "#{file_path}-#{new_file}-#{deleted_file}-#{renamed_file}" end end diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb index dc4d47c878b..fe7adb7bed6 100644 --- a/lib/gitlab/diff/file_collection/merge_request_diff.rb +++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb @@ -39,7 +39,7 @@ module Gitlab # hashes that represent serialized diff lines. # def cache_highlight!(diff_file) - item_key = diff_file.cache_key + item_key = diff_file.file_identifier if highlight_cache[item_key] highlight_diff_file_from_cache!(diff_file, highlight_cache[item_key]) diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb index b1a6d5fe0f6..f4d1505ea91 100644 --- a/lib/gitlab/ee_compat_check.rb +++ b/lib/gitlab/ee_compat_check.rb @@ -2,39 +2,38 @@ module Gitlab # Checks if a set of migrations requires downtime or not. class EeCompatCheck + CE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ce.git'.freeze EE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze + CHECK_DIR = Rails.root.join('ee_compat_check') + MAX_FETCH_DEPTH = 500 + IGNORED_FILES_REGEX = /(VERSION|CHANGELOG\.md:\d+)/.freeze - attr_reader :ce_branch, :check_dir, :ce_repo + attr_reader :repo_dir, :patches_dir, :ce_repo, :ce_branch - def initialize(branch:, check_dir:, ce_repo: nil) + def initialize(branch:, ce_repo: CE_REPO) + @repo_dir = CHECK_DIR.join('repo') + @patches_dir = CHECK_DIR.join('patches') @ce_branch = branch - @check_dir = check_dir - @ce_repo = ce_repo || 'https://gitlab.com/gitlab-org/gitlab-ce.git' + @ce_repo = ce_repo end def check ensure_ee_repo - delete_patches + ensure_patches_dir generate_patch(ce_branch, ce_patch_full_path) - Dir.chdir(check_dir) do - step("In the #{check_dir} directory") - - step("Pulling latest master", %w[git pull --ff-only origin master]) + Dir.chdir(repo_dir) do + step("In the #{repo_dir} directory") status = catch(:halt_check) do ce_branch_compat_check! - - delete_ee_branch_locally - + delete_ee_branch_locally! ee_branch_presence_check! - ee_branch_compat_check! end - delete_ee_branch_locally - delete_patches + delete_ee_branch_locally! if status.nil? true @@ -47,20 +46,43 @@ module Gitlab private def ensure_ee_repo - if Dir.exist?(check_dir) - step("#{check_dir} already exists") + if Dir.exist?(repo_dir) + step("#{repo_dir} already exists") else - cmd = %W[git clone --branch master --single-branch --depth 1 #{EE_REPO} #{check_dir}] - step("Cloning #{EE_REPO} into #{check_dir}", cmd) + cmd = %W[git clone --branch master --single-branch --depth 200 #{EE_REPO} #{repo_dir}] + step("Cloning #{EE_REPO} into #{repo_dir}", cmd) end end - def ce_branch_compat_check! - cmd = %W[git apply --check #{ce_patch_full_path}] - status = step("Checking if #{ce_patch_name} applies cleanly to EE/master", cmd) + def ensure_patches_dir + FileUtils.mkdir_p(patches_dir) + end + + def generate_patch(branch, patch_path) + FileUtils.rm(patch_path, force: true) + + depth = 0 + loop do + depth += 50 + cmd = %W[git fetch --depth #{depth} origin --prune +refs/heads/master:refs/remotes/origin/master] + Gitlab::Popen.popen(cmd) + _, status = Gitlab::Popen.popen(%w[git merge-base FETCH_HEAD HEAD]) + + raise "#{branch} is too far behind master, please rebase it!" if depth >= MAX_FETCH_DEPTH + break if status.zero? + end - if status.zero? - puts ce_applies_cleanly_msg(ce_branch) + step("Generating the patch against master in #{patch_path}") + output, status = Gitlab::Popen.popen(%w[git format-patch FETCH_HEAD --stdout]) + throw(:halt_check, :ko) unless status.zero? + + File.write(patch_path, output) + throw(:halt_check, :ko) unless File.exist?(patch_path) + end + + def ce_branch_compat_check! + if check_patch(ce_patch_full_path).zero? + puts applies_cleanly_msg(ce_branch) throw(:halt_check) end end @@ -80,10 +102,8 @@ module Gitlab step("Checking out origin/#{ee_branch}", %W[git checkout -b #{ee_branch} FETCH_HEAD]) generate_patch(ee_branch, ee_patch_full_path) - cmd = %W[git apply --check #{ee_patch_full_path}] - status = step("Checking if #{ee_patch_name} applies cleanly to EE/master", cmd) - unless status.zero? + unless check_patch(ee_patch_full_path).zero? puts puts ee_branch_doesnt_apply_cleanly_msg @@ -91,50 +111,49 @@ module Gitlab end puts - puts ee_applies_cleanly_msg + puts applies_cleanly_msg(ee_branch) end - def generate_patch(branch, filepath) - FileUtils.rm(filepath, force: true) + def check_patch(patch_path) + step("Checking out master", %w[git checkout master]) + step("Reseting to latest master", %w[git reset --hard origin/master]) - depth = 0 - loop do - depth += 10 - step("Fetching origin/master", %W[git fetch origin master --depth=#{depth}]) - status = step("Finding merge base with master", %W[git merge-base FETCH_HEAD #{branch}]) - - break if status.zero? || depth > 500 - end + step("Checking if #{patch_path} applies cleanly to EE/master") + output, status = Gitlab::Popen.popen(%W[git apply --check #{patch_path}]) - raise "#{branch} is too far behind master, please rebase it!" if depth > 500 + unless status.zero? + failed_files = output.lines.reduce([]) do |memo, line| + if line.start_with?('error: patch failed:') + file = line.sub(/\Aerror: patch failed: /, '') + memo << file unless file =~ IGNORED_FILES_REGEX + end + memo + end - step("Generating the patch against master") - output, status = Gitlab::Popen.popen(%w[git format-patch FETCH_HEAD --stdout]) - throw(:halt_check, :ko) unless status.zero? + if failed_files.empty? + status = 0 + else + puts "\nConflicting files:" + failed_files.each do |file| + puts " - #{file}" + end + end + end - File.write(filepath, output) - throw(:halt_check, :ko) unless File.exist?(filepath) + status end - def delete_ee_branch_locally + def delete_ee_branch_locally! command(%w[git checkout master]) step("Deleting the local #{ee_branch} branch", %W[git branch -D #{ee_branch}]) end - def delete_patches - step("Deleting #{ce_patch_full_path}") - FileUtils.rm(ce_patch_full_path, force: true) - - step("Deleting #{ee_patch_full_path}") - FileUtils.rm(ee_patch_full_path, force: true) - end - def ce_patch_name @ce_patch_name ||= "#{ce_branch}.patch" end def ce_patch_full_path - @ce_patch_full_path ||= File.expand_path(ce_patch_name, check_dir) + @ce_patch_full_path ||= patches_dir.join(ce_patch_name) end def ee_branch @@ -146,15 +165,18 @@ module Gitlab end def ee_patch_full_path - @ee_patch_full_path ||= File.expand_path(ee_patch_name, check_dir) + @ee_patch_full_path ||= patches_dir.join(ee_patch_name) end def step(desc, cmd = nil) puts "\n=> #{desc}\n" if cmd + start = Time.now puts "\n$ #{cmd.join(' ')}" - command(cmd) + status = command(cmd) + puts "\nFinished in #{Time.now - start} seconds" + status end end @@ -165,12 +187,12 @@ module Gitlab status end - def ce_applies_cleanly_msg(ce_branch) + def applies_cleanly_msg(branch) <<-MSG.strip_heredoc ================================================================= 🎉 Congratulations!! 🎉 - The #{ce_branch} branch applies cleanly to EE/master! + The #{branch} branch applies cleanly to EE/master! Much ❤️!! =================================================================\n @@ -211,7 +233,7 @@ module Gitlab # In the EE repo $ git fetch origin - $ git checkout -b #{ee_branch} FETCH_HEAD + $ git checkout -b #{ee_branch} origin/master $ git fetch #{ce_repo} #{ce_branch} $ git cherry-pick SHA # Repeat for all the commits you want to pick @@ -245,17 +267,5 @@ module Gitlab =================================================================\n MSG end - - def ee_applies_cleanly_msg - <<-MSG.strip_heredoc - ================================================================= - 🎉 Congratulations!! 🎉 - - The #{ee_branch} branch applies cleanly to EE/master! - - Much ❤️!! - =================================================================\n - MSG - end end end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 799794c0171..bcbf6455998 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/ldap/config.rb b/lib/gitlab/ldap/config.rb index f9bb5775323..6ea069d26df 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -92,6 +92,10 @@ module Gitlab options['timeout'].to_i end + def has_auth? + options['password'] || options['bind_dn'] + end + protected def base_config @@ -122,10 +126,6 @@ module Gitlab } } end - - def has_auth? - options['password'] || options['bind_dn'] - end end end end diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 2ae48a970ce..35c4194e87c 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -760,7 +760,7 @@ namespace :gitlab do end namespace :ldap do - task :check, [:limit] => :environment do |t, args| + task :check, [:limit] => :environment do |_, args| # Only show up to 100 results because LDAP directories can be very big. # This setting only affects the `rake gitlab:check` script. args.with_defaults(limit: 100) @@ -768,7 +768,7 @@ namespace :gitlab do start_checking "LDAP" if Gitlab::LDAP::Config.enabled? - print_users(args.limit) + check_ldap(args.limit) else puts 'LDAP is disabled in config/gitlab.yml' end @@ -776,21 +776,42 @@ namespace :gitlab do finished_checking "LDAP" end - def print_users(limit) - puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)" - + def check_ldap(limit) servers = Gitlab::LDAP::Config.providers servers.each do |server| puts "Server: #{server}" - Gitlab::LDAP::Adapter.open(server) do |adapter| - users = adapter.users(adapter.config.uid, '*', limit) - users.each do |user| - puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}" + + begin + Gitlab::LDAP::Adapter.open(server) do |adapter| + check_ldap_auth(adapter) + + puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)" + + users = adapter.users(adapter.config.uid, '*', limit) + users.each do |user| + puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}" + end end + rescue Net::LDAP::ConnectionRefusedError, Errno::ECONNREFUSED => e + puts "Could not connect to the LDAP server: #{e.message}".color(:red) end end end + + def check_ldap_auth(adapter) + auth = adapter.config.has_auth? + + if auth && adapter.ldap.bind + message = 'Success'.color(:green) + elsif auth + message = 'Failed. Check `bind_dn` and `password` configuration values'.color(:red) + else + message = 'Anonymous. No `bind_dn` or `password` configured'.color(:yellow) + end + + puts "LDAP authentication... #{message}" + end end namespace :repo do diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake index 5ee99dfc810..3117075b08b 100644 --- a/lib/tasks/gitlab/dev.rake +++ b/lib/tasks/gitlab/dev.rake @@ -1,18 +1,22 @@ namespace :gitlab do namespace :dev do desc 'Checks if the branch would apply cleanly to EE' - task ee_compat_check: :environment do - return if defined?(Gitlab::License) - return unless ENV['CI'] + task :ee_compat_check, [:branch] => :environment do |_, args| + opts = + if ENV['CI'] + { + branch: ENV['CI_BUILD_REF_NAME'], + ce_repo: ENV['CI_BUILD_REPO'] + } + else + unless args[:branch] + puts "Must specify a branch as an argument".color(:red) + exit 1 + end + args + end - success = - Gitlab::EeCompatCheck.new( - branch: ENV['CI_BUILD_REF_NAME'], - check_dir: File.expand_path('ee-compat-check', __dir__), - ce_repo: ENV['CI_BUILD_REPO'] - ).check - - if success + if Gitlab::EeCompatCheck.new(opts || {}).check exit 0 else exit 1 diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index dd4a86b1e31..bfd88a254f1 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/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index 6c938bdead8..3934c936f20 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -182,6 +182,20 @@ feature 'Expand and collapse diffs', js: true, feature: true do end end end + + context 'expanding a diff when symlink was converted to a regular file' do + let(:branch) { 'symlink-expand-diff' } + + it 'shows the content of the regular file' do + expect(page).to have_content('This diff is collapsed') + expect(page).to have_no_content('No longer a symlink') + + find('.click-to-expand').click + wait_for_ajax + + expect(page).to have_content('No longer a symlink') + end + end end context 'visiting a commit without collapsed diffs' do diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb new file mode 100644 index 00000000000..476eca17a9d --- /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 00000000000..a2791b57544 --- /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/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb index 755f4eb1b0b..ab901e74617 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, noteable: issue, 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/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 9c7c79f57c6..837e7afa7e8 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -61,7 +61,7 @@ describe DiffHelper do describe '#diff_line_content' do it 'returns non breaking space when line is empty' do - expect(diff_line_content(nil)).to eq(' ') + expect(diff_line_content(nil)).to eq(' ') end it 'returns the line itself' do diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb index dca7f997570..a6d2ea11fcc 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 f181125156b..0140a91c7ba 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] + + link = reference_link(project: project.id, reference_type: 'test') + doc = filter(link, current_user: user) - expect(doc.css('a').length).to eq 0 + 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 + + it 'handles invalid references' do + link = reference_link(project: 12345, reference_type: 'test') - expect { filter(link) }.not_to raise_error + 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 9095d2b1345..aa127f0179d 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 - - 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).to receive(:can_read_reference?).with(user, project) - 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 0b76d29fce0..412ffa77c36 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 ba982f38542..96e55b0997a 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 a6ef8394fe7..50a5d1a19ba 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 85cfe728b6a..6873b7b85f9 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 77fda47f0e7..8c540d35ddd 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 cf89ad598ea..cb69ca16800 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 6aa45a22cc4..2d4d589ae34 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 59127b7c5d1..d217a775802 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 4e7f82a6e09..fafc2cec546 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 de3f64249a2..1bbaca0739a 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 00000000000..01b2a55b63c --- /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 f045463c1cb..6b3dfebd85d 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 62aa212f1f6..f1d0a190002 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 diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 576cda595bb..576aa5c366f 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/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index 835853a83a4..f5ebe703083 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -1,20 +1,51 @@ require 'spec_helper' describe Gitlab::LDAP::Config, lib: true do - let(:config) { Gitlab::LDAP::Config.new provider } - let(:provider) { 'ldapmain' } + include LdapHelpers + + let(:config) { Gitlab::LDAP::Config.new('ldapmain') } describe '#initalize' do it 'requires a provider' do expect{ Gitlab::LDAP::Config.new }.to raise_error ArgumentError end - it "works" do + it 'works' do expect(config).to be_a described_class end - it "raises an error if a unknow provider is used" do + it 'raises an error if a unknown provider is used' do expect{ Gitlab::LDAP::Config.new 'unknown' }.to raise_error(RuntimeError) end end + + describe '#has_auth?' do + it 'is true when password is set' do + stub_ldap_config( + options: { + 'bind_dn' => 'uid=admin,dc=example,dc=com', + 'password' => 'super_secret' + } + ) + + expect(config.has_auth?).to be_truthy + end + + it 'is true when bind_dn is set and password is empty' do + stub_ldap_config( + options: { + 'bind_dn' => 'uid=admin,dc=example,dc=com', + 'password' => '' + } + ) + + expect(config.has_auth?).to be_truthy + end + + it 'is false when password and bind_dn are not set' do + stub_ldap_config(options: { 'bind_dn' => nil, 'password' => nil }) + + expect(config.has_auth?).to be_falsey + end + end end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 7b4ccc83915..bf0ab9635fd 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/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 5eb14dc6bd2..71b7628ef10 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -524,4 +524,78 @@ describe Ci::Pipeline, models: true do expect(pipeline.merge_requests).to be_empty end end + + describe 'notifications when pipeline success or failed' do + let(:project) { create(:project) } + + let(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit('master').sha, + user: create(:user)) + end + + before do + reset_delivered_emails! + + project.team << [pipeline.user, Gitlab::Access::DEVELOPER] + + perform_enqueued_jobs do + pipeline.enqueue + pipeline.run + end + end + + shared_examples 'sending a notification' do + it 'sends an email' do + should_only_email(pipeline.user, kind: :bcc) + end + end + + shared_examples 'not sending any notification' do + it 'does not send any email' do + should_not_email_anyone + end + end + + context 'with success pipeline' do + before do + perform_enqueued_jobs do + pipeline.succeed + end + end + + it_behaves_like 'sending a notification' + end + + context 'with failed pipeline' do + before do + perform_enqueued_jobs do + pipeline.drop + end + end + + it_behaves_like 'sending a notification' + end + + context 'with skipped pipeline' do + before do + perform_enqueued_jobs do + pipeline.skip + end + end + + it_behaves_like 'not sending any notification' + end + + context 'with cancelled pipeline' do + before do + perform_enqueued_jobs do + pipeline.cancel + end + end + + it_behaves_like 'not sending any notification' + end + end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index a9603074c32..6e987967ca5 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/event_spec.rb b/spec/models/event_spec.rb index aca49be2942..29a3af68a9b 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) diff --git a/spec/models/group_label_spec.rb b/spec/models/group_label_spec.rb index 85eb889225b..2369658bf78 100644 --- a/spec/models/group_label_spec.rb +++ b/spec/models/group_label_spec.rb @@ -18,7 +18,7 @@ describe GroupLabel, models: true do end describe '#to_reference' do - let(:label) { create(:group_label) } + let(:label) { create(:group_label, title: 'feature') } context 'using id' do it 'returns a String reference to the object' do diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb new file mode 100644 index 00000000000..d79f929f7a1 --- /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 60d30eb7418..300425767ed 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,17 +102,17 @@ describe Issue, models: true do it 'returns the merge request to close this issue' do mr - 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).to eq([]) + 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 @@ -218,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 @@ -314,6 +314,22 @@ describe Issue, models: true do end describe '#visible_to_user?' do + context 'without a user' do + let(:issue) { build(:issue) } + + it 'returns true when the issue is publicly visible' do + expect(issue).to receive(:publicly_visible?).and_return(true) + + expect(issue.visible_to_user?).to eq(true) + end + + it 'returns false when the issue is not publicly visible' do + expect(issue).to receive(:publicly_visible?).and_return(false) + + expect(issue.visible_to_user?).to eq(false) + end + end + context 'with a user' do let(:user) { build(:user) } let(:issue) { build(:issue) } @@ -329,26 +345,24 @@ describe Issue, models: true do expect(issue.visible_to_user?(user)).to eq(false) end - end - context 'without a user' do - let(:issue) { build(:issue) } + it 'returns false when feature is disabled' do + expect(issue).not_to receive(:readable_by?) - it 'returns true when the issue is publicly visible' do - expect(issue).to receive(:publicly_visible?).and_return(true) + issue.project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) - expect(issue.visible_to_user?).to eq(true) + expect(issue.visible_to_user?(user)).to eq(false) end - it 'returns false when the issue is not publicly visible' do - expect(issue).to receive(:publicly_visible?).and_return(false) + it 'returns false when restricted for members' do + expect(issue).not_to receive(:readable_by?) - expect(issue.visible_to_user?).to eq(false) + issue.project.project_feature.update_attribute(:issues_access_level, ProjectFeature::PRIVATE) + + expect(issue.visible_to_user?(user)).to eq(false) end end - end - describe '#readable_by?' do describe 'with a regular user that is not a team member' do let(:user) { create(:user) } @@ -358,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 @@ -375,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 @@ -393,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 @@ -410,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 @@ -447,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 @@ -467,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 @@ -487,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 @@ -505,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 @@ -523,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 @@ -539,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 @@ -555,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_services/pipeline_email_service_spec.rb b/spec/models/project_services/pipeline_email_service_spec.rb index 1368a2925e8..4f56bceda44 100644 --- a/spec/models/project_services/pipeline_email_service_spec.rb +++ b/spec/models/project_services/pipeline_email_service_spec.rb @@ -13,7 +13,7 @@ describe PipelinesEmailService do end before do - ActionMailer::Base.deliveries.clear + reset_delivered_emails! end describe 'Validations' do @@ -23,14 +23,6 @@ describe PipelinesEmailService do end it { is_expected.to validate_presence_of(:recipients) } - - context 'when pusher is added' do - before do - subject.add_pusher = true - end - - it { is_expected.not_to validate_presence_of(:recipients) } - end end context 'when service is inactive' do @@ -66,8 +58,7 @@ describe PipelinesEmailService do end it 'sends email' do - sent_to = ActionMailer::Base.deliveries.flat_map(&:to) - expect(sent_to).to contain_exactly(recipient) + should_only_email(double(notification_email: recipient), kind: :bcc) end end @@ -79,7 +70,7 @@ describe PipelinesEmailService do end it 'does not send email' do - expect(ActionMailer::Base.deliveries).to be_empty + should_not_email_anyone end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 12989d4db53..fe26b4ac18c 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -113,6 +113,26 @@ describe Repository, models: true do end end + describe '#ref_exists?' do + context 'when ref exists' do + it 'returns true' do + expect(repository.ref_exists?('refs/heads/master')).to be true + end + end + + context 'when ref does not exist' do + it 'returns false' do + expect(repository.ref_exists?('refs/heads/non-existent')).to be false + end + end + + context 'when ref format is incorrect' do + it 'returns false' do + expect(repository.ref_exists?('refs/heads/invalid:master')).to be false + end + end + end + describe '#last_commit_for_path' do subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id } @@ -197,6 +217,35 @@ describe Repository, models: true do end end + describe '#commit' do + context 'when ref exists' do + it 'returns commit object' do + expect(repository.commit('master')) + .to be_an_instance_of Commit + end + end + + context 'when ref does not exist' do + it 'returns nil' do + expect(repository.commit('non-existent-ref')).to be_nil + end + end + + context 'when ref is not valid' do + context 'when preceding tree element exists' do + it 'returns nil' do + expect(repository.commit('master:ref')).to be_nil + end + end + + context 'when preceding tree element does not exist' do + it 'returns nil' do + expect(repository.commit('non-existent:ref')).to be_nil + end + end + end + end + describe "#commit_dir" do it "commits a change that creates a new directory" do expect do diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 7b47bf5afc1..b29a13b1d8b 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -68,6 +68,24 @@ describe API::API, api: true do end end + describe 'GET /groups/owned' do + context 'when unauthenticated' do + it 'returns authentication error' do + get api('/groups/owned') + expect(response).to have_http_status(401) + end + end + + context 'when authenticated as group owner' do + it 'returns an array of groups the user owns' do + get api('/groups/owned', user2) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(group2.name) + end + end + end + describe "GET /groups/:id" do context "when authenticated as user" do it "returns one of user1's groups" do diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 2ff90b6deac..5d84976c9c3 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -15,7 +15,7 @@ describe API::API, api: true do describe 'GET /projects/:id/labels' do it 'returns all available labels to the project' do group = create(:group) - group_label = create(:group_label, group: group) + group_label = create(:group_label, title: 'feature', group: group) project.update(group: group) expected_keys = [ 'id', 'name', 'color', 'description', diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index dd192bea432..62327f64e50 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -123,6 +123,15 @@ describe API::API, api: true do expect(json_response['title']).to eq('updated title') end + it 'removes a due date if nil is passed' do + milestone.update!(due_date: "2016-08-05") + + put api("/projects/#{project.id}/milestones/#{milestone.id}", user), due_date: nil + + expect(response).to have_http_status(200) + expect(json_response['due_date']).to be_nil + end + it 'returns a 404 error if milestone id not found' do put api("/projects/#{project.id}/milestones/1234", user), title: 'updated title' diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 3c8f0ac531a..d6e9fd2c4b2 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -175,6 +175,30 @@ describe API::API, api: true do end end + describe 'GET /projects/owned' do + before do + project3 + project4 + end + + context 'when unauthenticated' do + it 'returns authentication error' do + get api('/projects/owned') + expect(response).to have_http_status(401) + end + end + + context 'when authenticated as project owner' do + it 'returns an array of projects the user owns' do + get api('/projects/owned', user4) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(project4.name) + expect(json_response.first['owner']['username']).to eq(user4.username) + end + end + end + describe 'GET /projects/visible' do let(:public_project) { create(:project, :public) } diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index f46f016135e..99414270be6 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -226,7 +226,7 @@ describe API::Runners, api: true do context 'authorized user' do context 'when runner is shared' do it 'does not update runner' do - put api("/runners/#{shared_runner.id}", user) + put api("/runners/#{shared_runner.id}", user), description: 'test' expect(response).to have_http_status(403) end @@ -234,7 +234,7 @@ describe API::Runners, api: true do context 'when runner is not shared' do it 'does not update runner without access to it' do - put api("/runners/#{specific_runner.id}", user2) + put api("/runners/#{specific_runner.id}", user2), description: 'test' expect(response).to have_http_status(403) end diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb index acad1365ace..e3f22b4c578 100644 --- a/spec/requests/api/session_spec.rb +++ b/spec/requests/api/session_spec.rb @@ -67,22 +67,24 @@ describe API::API, api: true do end context "when empty password" do - it "returns authentication error" do + it "returns authentication error with email" do post api("/session"), email: user.email - expect(response).to have_http_status(401) - expect(json_response['email']).to be_nil - expect(json_response['private_token']).to be_nil + expect(response).to have_http_status(400) + end + + it "returns authentication error with username" do + post api("/session"), email: user.username + + expect(response).to have_http_status(400) end end context "when empty name" do it "returns authentication error" do post api("/session"), password: user.password - expect(response).to have_http_status(401) - expect(json_response['email']).to be_nil - expect(json_response['private_token']).to be_nil + expect(response).to have_http_status(400) end end end diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 82bba1ce8a4..8ba2eccf66c 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -68,7 +68,7 @@ describe API::API do it 'validates variables to be a hash' do post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master') expect(response).to have_http_status(400) - expect(json_response['message']).to eq('variables needs to be a hash') + expect(json_response['error']).to eq('variables is invalid') end it 'validates variables needs to be a map of key-valued strings' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index ae8639d78d5..34d1f557e4b 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -48,6 +48,17 @@ describe API::API, api: true do end['username']).to eq(username) end + it "returns an array of blocked users" do + ldap_blocked_user + create(:user, state: 'blocked') + + get api("/users?blocked=true", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response).to all(include('state' => /(blocked|ldap_blocked)/)) + end + it "returns one user" do get api("/users?username=#{omniauth_user.username}", user) expect(response).to have_http_status(200) @@ -69,6 +80,16 @@ describe API::API, api: true do expect(json_response.first.keys).to include 'last_sign_in_at' expect(json_response.first.keys).to include 'confirmed_at' end + + it "returns an array of external users" do + create(:user, external: true) + + get api("/users?external=true", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response).to all(include('external' => true)) + end end end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 27f0fd22ae6..f1728d61def 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 f0ef155bd7b..a3e7844b2f3 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/services/ci/send_pipeline_notification_service_spec.rb b/spec/services/ci/send_pipeline_notification_service_spec.rb deleted file mode 100644 index 288302cc94f..00000000000 --- a/spec/services/ci/send_pipeline_notification_service_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'spec_helper' - -describe Ci::SendPipelineNotificationService, services: true do - let(:pipeline) do - create(:ci_pipeline, - project: project, - sha: project.commit('master').sha, - user: user, - status: status) - end - - let(:project) { create(:project) } - let(:user) { create(:user) } - - subject{ described_class.new(pipeline) } - - describe '#execute' do - before do - reset_delivered_emails! - end - - shared_examples 'sending emails' do - it 'sends an email to pipeline user' do - perform_enqueued_jobs do - subject.execute([user.email]) - end - - email = ActionMailer::Base.deliveries.last - expect(email.subject).to include(email_subject) - expect(email.to).to eq([user.email]) - end - end - - context 'with success pipeline' do - let(:status) { 'success' } - let(:email_subject) { "Pipeline ##{pipeline.id} has succeeded" } - - it_behaves_like 'sending emails' - end - - context 'with failed pipeline' do - let(:status) { 'failed' } - let(:email_subject) { "Pipeline ##{pipeline.id} has failed" } - - it_behaves_like 'sending emails' - end - end -end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 699b9925b4e..8ce35354c22 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -17,7 +17,7 @@ describe NotificationService, services: true do it 'sends no emails when no new mentions are present' do send_notifications - expect(ActionMailer::Base.deliveries).to be_empty + should_not_email_anyone end it 'emails new mentions with a watch level higher than participant' do @@ -27,7 +27,7 @@ describe NotificationService, services: true do it 'does not email new mentions with a watch level equal to or less than participant' do send_notifications(@u_participating, @u_mentioned) - expect(ActionMailer::Base.deliveries).to be_empty + should_not_email_anyone end end @@ -79,7 +79,7 @@ describe NotificationService, services: true do # Ensure create SentNotification by noteable = issue 6 times, not noteable = note expect(SentNotification).to receive(:record).with(issue, any_args).exactly(8).times - ActionMailer::Base.deliveries.clear + reset_delivered_emails! notification.new_note(note) @@ -111,7 +111,7 @@ describe NotificationService, services: true do context 'participating' do context 'by note' do before do - ActionMailer::Base.deliveries.clear + reset_delivered_emails! note.author = @u_lazy_participant note.save notification.new_note(note) @@ -134,7 +134,7 @@ describe NotificationService, services: true do @u_watcher.notification_settings_for(note.project).participating! @u_watcher.notification_settings_for(note.project.group).global! update_custom_notification(:new_note, @u_custom_global) - ActionMailer::Base.deliveries.clear + reset_delivered_emails! end it do @@ -173,7 +173,7 @@ describe NotificationService, services: true do expect(SentNotification).to receive(:record).with(confidential_issue, any_args).exactly(4).times - ActionMailer::Base.deliveries.clear + reset_delivered_emails! notification.new_note(note) @@ -196,7 +196,7 @@ describe NotificationService, services: true do before do build_team(note.project) note.project.team << [note.author, :master] - ActionMailer::Base.deliveries.clear + reset_delivered_emails! end describe '#new_note' do @@ -238,7 +238,7 @@ describe NotificationService, services: true do before do build_team(note.project) note.project.team << [note.author, :master] - ActionMailer::Base.deliveries.clear + reset_delivered_emails! end describe '#new_note' do @@ -273,7 +273,7 @@ describe NotificationService, services: true do before do build_team(note.project) - ActionMailer::Base.deliveries.clear + reset_delivered_emails! allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer) update_custom_notification(:new_note, @u_guest_custom, project) update_custom_notification(:new_note, @u_custom_global) @@ -348,7 +348,7 @@ describe NotificationService, services: true do before do build_team(issue.project) add_users_with_subscription(issue.project, issue) - ActionMailer::Base.deliveries.clear + reset_delivered_emails! update_custom_notification(:new_issue, @u_guest_custom, project) update_custom_notification(:new_issue, @u_custom_global) end @@ -408,7 +408,7 @@ describe NotificationService, services: true do label.toggle_subscription(guest) label.toggle_subscription(admin) - ActionMailer::Base.deliveries.clear + reset_delivered_emails! notification.new_issue(confidential_issue, @u_disabled) @@ -604,7 +604,7 @@ describe NotificationService, services: true do label_2.toggle_subscription(guest) label_2.toggle_subscription(admin) - ActionMailer::Base.deliveries.clear + reset_delivered_emails! notification.relabeled_issue(confidential_issue, [label_2], @u_disabled) @@ -733,7 +733,7 @@ describe NotificationService, services: true do add_users_with_subscription(merge_request.target_project, merge_request) update_custom_notification(:new_merge_request, @u_guest_custom, project) update_custom_notification(:new_merge_request, @u_custom_global) - ActionMailer::Base.deliveries.clear + reset_delivered_emails! end describe '#new_merge_request' do @@ -1111,7 +1111,7 @@ describe NotificationService, services: true do before do build_team(project) - ActionMailer::Base.deliveries.clear + reset_delivered_emails! end describe '#project_was_moved' do diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb index 62a5b46d47b..75c95d70951 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/email_helpers.rb b/spec/support/email_helpers.rb index 0bfc4685532..3e979f2f470 100644 --- a/spec/support/email_helpers.rb +++ b/spec/support/email_helpers.rb @@ -1,23 +1,33 @@ module EmailHelpers - def sent_to_user?(user) - ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1 + def sent_to_user?(user, recipients = email_recipients) + recipients.include?(user.notification_email) end def reset_delivered_emails! ActionMailer::Base.deliveries.clear end - def should_only_email(*users) - users.each {|user| should_email(user) } - recipients = ActionMailer::Base.deliveries.flat_map(&:to) + def should_only_email(*users, kind: :to) + recipients = email_recipients(kind: kind) + + users.each { |user| should_email(user, recipients) } + expect(recipients.count).to eq(users.count) end - def should_email(user) - expect(sent_to_user?(user)).to be_truthy + def should_email(user, recipients = email_recipients) + expect(sent_to_user?(user, recipients)).to be_truthy + end + + def should_not_email(user, recipients = email_recipients) + expect(sent_to_user?(user, recipients)).to be_falsey + end + + def should_not_email_anyone + expect(ActionMailer::Base.deliveries).to be_empty end - def should_not_email(user) - expect(sent_to_user?(user)).to be_falsey + def email_recipients(kind: :to) + ActionMailer::Base.deliveries.flat_map(&kind) end end diff --git a/spec/support/notify_shared_examples.rb b/spec/support/notify_shared_examples.rb index 3956d05060b..49867aa5cc4 100644 --- a/spec/support/notify_shared_examples.rb +++ b/spec/support/notify_shared_examples.rb @@ -7,7 +7,7 @@ shared_context 'gitlab email notification' do let(:new_user_address) { 'newguy@example.com' } before do - ActionMailer::Base.deliveries.clear + reset_delivered_emails! email = recipient.emails.create(email: "notifications@example.com") recipient.update_attribute(:notification_email, email.email) stub_incoming_email_setting(enabled: true, address: "reply+%{key}@#{Gitlab.config.gitlab.host}") 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 00000000000..4621d17549b --- /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 00000000000..8eb74635a60 --- /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/support/test_env.rb b/spec/support/test_env.rb index 778e665500d..103f7542286 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -23,6 +23,7 @@ module TestEnv 'binary-encoding' => '7b1cf43', 'gitattributes' => '5a62481', 'expand-collapse-diffs' => '4842455', + 'symlink-expand-diff' => '81e6355', 'expand-collapse-files' => '025db92', 'expand-collapse-lines' => '238e82d', 'video' => '8879059', diff --git a/spec/tasks/gitlab/check_rake_spec.rb b/spec/tasks/gitlab/check_rake_spec.rb new file mode 100644 index 00000000000..538ff952bf4 --- /dev/null +++ b/spec/tasks/gitlab/check_rake_spec.rb @@ -0,0 +1,51 @@ +require 'rake_helper' + +describe 'gitlab:ldap:check rake task' do + include LdapHelpers + + before do + Rake.application.rake_require 'tasks/gitlab/check' + + stub_warn_user_is_not_gitlab + end + + context 'when LDAP is not enabled' do + it 'does not attempt to bind or search for users' do + expect(Gitlab::LDAP::Config).not_to receive(:providers) + expect(Gitlab::LDAP::Adapter).not_to receive(:open) + + run_rake_task('gitlab:ldap:check') + end + end + + context 'when LDAP is enabled' do + let(:ldap) { double(:ldap) } + let(:adapter) { ldap_adapter('ldapmain', ldap) } + + before do + allow(Gitlab::LDAP::Config) + .to receive_messages( + enabled?: true, + providers: ['ldapmain'] + ) + allow(Gitlab::LDAP::Adapter).to receive(:open).and_yield(adapter) + allow(adapter).to receive(:users).and_return([]) + end + + it 'attempts to bind using credentials' do + stub_ldap_config(has_auth?: true) + + expect(ldap).to receive(:bind) + + run_rake_task('gitlab:ldap:check') + end + + it 'searches for 100 LDAP users' do + stub_ldap_config(uid: 'uid') + + expect(adapter).to receive(:users).with('uid', '*', 100) + + run_rake_task('gitlab:ldap:check') + end + end +end diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb index 788b92c1b84..a1aa336361a 100644 --- a/spec/workers/build_email_worker_spec.rb +++ b/spec/workers/build_email_worker_spec.rb @@ -24,7 +24,7 @@ describe BuildEmailWorker do end it "gracefully handles an input SMTP error" do - ActionMailer::Base.deliveries.clear + reset_delivered_emails! allow(Notify).to receive(:build_success_email).and_raise(Net::SMTPFatalError) subject.perform(build.id, [user.email], data.stringify_keys) diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb index 036d037f3f9..fc652f6f4c3 100644 --- a/spec/workers/emails_on_push_worker_spec.rb +++ b/spec/workers/emails_on_push_worker_spec.rb @@ -87,7 +87,7 @@ describe EmailsOnPushWorker do context "when there is an SMTP error" do before do - ActionMailer::Base.deliveries.clear + reset_delivered_emails! allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError) allow(subject).to receive_message_chain(:logger, :info) perform @@ -112,7 +112,7 @@ describe EmailsOnPushWorker do original.call(Mail.new(mail.encoded)) end - ActionMailer::Base.deliveries.clear + reset_delivered_emails! end it "sends the mail to each of the recipients" do diff --git a/spec/workers/pipeline_notification_worker_spec.rb b/spec/workers/pipeline_notification_worker_spec.rb new file mode 100644 index 00000000000..d487a719680 --- /dev/null +++ b/spec/workers/pipeline_notification_worker_spec.rb @@ -0,0 +1,131 @@ +require 'spec_helper' + +describe PipelineNotificationWorker do + let(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit('master').sha, + user: pusher, + status: status) + end + + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:pusher) { user } + let(:watcher) { pusher } + + describe '#execute' do + before do + reset_delivered_emails! + pipeline.project.team << [pusher, Gitlab::Access::DEVELOPER] + end + + context 'when watcher has developer access' do + before do + pipeline.project.team << [watcher, Gitlab::Access::DEVELOPER] + end + + shared_examples 'sending emails' do + it 'sends emails' do + perform_enqueued_jobs do + subject.perform(pipeline.id) + end + + emails = ActionMailer::Base.deliveries + actual = emails.flat_map(&:bcc).sort + expected_receivers = receivers.map(&:email).uniq.sort + + expect(actual).to eq(expected_receivers) + expect(emails.size).to eq(1) + expect(emails.last.subject).to include(email_subject) + end + end + + context 'with success pipeline' do + let(:status) { 'success' } + let(:email_subject) { "Pipeline ##{pipeline.id} has succeeded" } + let(:receivers) { [pusher, watcher] } + + it_behaves_like 'sending emails' + + context 'with pipeline from someone else' do + let(:pusher) { create(:user) } + let(:watcher) { user } + + context 'with success pipeline notification on' do + before do + watcher.global_notification_setting. + update(level: 'custom', success_pipeline: true) + end + + it_behaves_like 'sending emails' + end + + context 'with success pipeline notification off' do + let(:receivers) { [pusher] } + + before do + watcher.global_notification_setting. + update(level: 'custom', success_pipeline: false) + end + + it_behaves_like 'sending emails' + end + end + + context 'with failed pipeline' do + let(:status) { 'failed' } + let(:email_subject) { "Pipeline ##{pipeline.id} has failed" } + + it_behaves_like 'sending emails' + + context 'with pipeline from someone else' do + let(:pusher) { create(:user) } + let(:watcher) { user } + + context 'with failed pipeline notification on' do + before do + watcher.global_notification_setting. + update(level: 'custom', failed_pipeline: true) + end + + it_behaves_like 'sending emails' + end + + context 'with failed pipeline notification off' do + let(:receivers) { [pusher] } + + before do + watcher.global_notification_setting. + update(level: 'custom', failed_pipeline: false) + end + + it_behaves_like 'sending emails' + end + end + end + end + end + + context 'when watcher has no read_build access' do + let(:status) { 'failed' } + let(:email_subject) { "Pipeline ##{pipeline.id} has failed" } + let(:watcher) { create(:user) } + + before do + pipeline.project.team << [watcher, Gitlab::Access::GUEST] + + watcher.global_notification_setting. + update(level: 'custom', failed_pipeline: true) + + perform_enqueued_jobs do + subject.perform(pipeline.id) + end + end + + it 'does not send emails' do + should_only_email(pusher, kind: :bcc) + end + end + end +end |