diff options
178 files changed, 3025 insertions, 819 deletions
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md index ac38f0c9521..6d7d88c6791 100644 --- a/.gitlab/issue_templates/Bug.md +++ b/.gitlab/issue_templates/Bug.md @@ -21,6 +21,8 @@ logs, and code as it's very hard to read otherwise.) ### Output of checks +(If you are reporting a bug on GitLab.com, write: This bug happens on GitLab.com) + #### Results of GitLab application Check (For installations with omnibus-gitlab package run and paste the output of: diff --git a/CHANGELOG.md b/CHANGELOG.md index 86b30d2832d..e03123111c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.14.3 (2016-12-02) + +- Pass commit data to ProcessCommitWorker to reduce Git overhead. !7744 +- Speed up issuable dashboards. +- Don't change relative URLs to absolute URLs in the Help page. +- Fixes "ActionView::Template::Error: undefined method `text?` for nil:NilClass" on MR pages. +- Fix branch validation for GitHub PR where repo/fork was renamed/deleted. +- Validate state param when filtering issuables. + ## 8.14.2 (2016-12-01) - Remove caching of events data. !6578 @@ -242,6 +251,11 @@ entry. - Fix "Without projects" filter. !6611 (Ben Bodenmiller) - Fix 404 when visit /projects page +## 8.13.8 (2016-12-02) + +- Pass tag SHA to post-receive hook when tag is created via UI. !7700 +- Validate state param when filtering issuables. + ## 8.13.7 (2016-11-28) - fixes 500 error on project show when user is not logged in and project is still empty. !7376 diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 4aaff7d04f1..4d4835568ed 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -40,3 +40,6 @@ @import "framework/blank"; @import "framework/wells.scss"; @import "framework/page-header.scss"; +@import "framework/awards.scss"; +@import "framework/images.scss"; +@import "framework/broadcast-messages"; diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/framework/awards.scss index c13cb4a02b2..c13cb4a02b2 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 57db5eaa2b3..95c02499271 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -1,8 +1,3 @@ -.light-well { - background-color: $background-color; - padding: 15px; -} - .centered-light-block { text-align: center; color: $gl-gray; @@ -274,6 +269,10 @@ } } + .emoji-icon { + display: inline-block; + } + @media(max-width: $screen-xs-max) { margin-top: 50px; text-align: center; diff --git a/app/assets/stylesheets/framework/broadcast-messages.scss b/app/assets/stylesheets/framework/broadcast-messages.scss new file mode 100644 index 00000000000..9b54fb94cdc --- /dev/null +++ b/app/assets/stylesheets/framework/broadcast-messages.scss @@ -0,0 +1,21 @@ +.broadcast-message { + @extend .alert-warning; + padding: 10px; + text-align: center; + + div, + p { + display: inline; + margin: 0; + + a { + color: inherit; + text-decoration: underline; + } + } +} + +.broadcast-message-preview { + @extend .broadcast-message; + margin-bottom: 20px; +} diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 16646e33a4b..600bf17259b 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -379,7 +379,9 @@ table { border-top: 1px solid $border-color; } -.hide-bottom-border { border-bottom: none !important; } +.hide-bottom-border { + border-bottom: none !important; +} .gl-accessibility { &:focus { @@ -396,3 +398,13 @@ table { z-index: 1; } } + +.str-truncated { + &-60 { + @include str-truncated(60%); + } + + &-100 { + @include str-truncated(100%); + } +} diff --git a/app/assets/stylesheets/pages/appearances.scss b/app/assets/stylesheets/framework/images.scss index 878f44116ba..878f44116ba 100644 --- a/app/assets/stylesheets/pages/appearances.scss +++ b/app/assets/stylesheets/framework/images.scss diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index db8677433bb..ed4b60faf92 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -106,13 +106,13 @@ ul.task-list { } } +// Generic content list ul.content-list { @include basic-list; - margin: 0; padding: 0; - > li { + li { border-color: $table-border-color; font-size: $list-font-size; color: $list-text-color; @@ -193,6 +193,41 @@ ul.content-list { } } +// Content list using flexbox +.flex-list { + .flex-row { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + white-space: nowrap; + } + + .row-main-content { + flex: 1 1 auto; + overflow: hidden; + padding-right: 8px; + } + + .row-title { + font-weight: 600; + } + + .row-second-line { + display: block; + } + + .dropdown { + .btn-block { + margin-bottom: 0; + line-height: inherit; + } + } + + .label-default { + color: $btn-transparent-color; + } +} + .panel > .content-list > li { padding: $gl-padding-top $gl-padding; diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index c84a71a624d..69da520f21f 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -268,6 +268,16 @@ width: auto; } } + + &.multi-line { + .nav-text { + line-height: 20px; + } + + .nav-controls { + padding: 17px 0; + } + } } .layout-nav { diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index a5f36c177fc..5d0ca63ea08 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -34,6 +34,10 @@ table { background-color: $background-color; font-weight: normal; border-bottom: none; + + &.wide { + width: 55%; + } } td { @@ -42,3 +46,16 @@ table { } } } + +.responsive-table { + @media (max-width: $screen-sm-max) { + th { + width: 100%; + } + + td { + width: 100%; + float: left; + } + } +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 647dcfc5187..18716813c48 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -427,12 +427,6 @@ $common-gray-dark: #444; $common-red: $gl-text-red; $common-green: $gl-text-green; - -/* -* Dashboard -*/ -$dashboard-project-access-icon-color: #888; - /* * Editor */ diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss index 192939f4527..f2860dfe84d 100644 --- a/app/assets/stylesheets/framework/wells.scss +++ b/app/assets/stylesheets/framework/wells.scss @@ -43,3 +43,16 @@ background-color: $well-expand-item; } } + +.light-well { + background-color: $background-color; + padding: 15px; +} + +.well-centered { + h1 { + font-weight: normal; + text-align: center; + font-size: 48px; + } +} diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss deleted file mode 100644 index 44eac21b143..00000000000 --- a/app/assets/stylesheets/pages/admin.scss +++ /dev/null @@ -1,168 +0,0 @@ -/** - * Admin area - * - */ -.admin-dashboard { - .data { - a { - h1 { - line-height: 48px; - font-size: 48px; - padding: 20px; - text-align: center; - font-weight: normal; - } - } - } - - .str-truncated { - max-width: 60%; - } -} - -.admin-filter form { - .select2-container { - width: 100%; - } - - .controls { - margin-left: 130px; - } - - .form-actions { - padding-left: 130px; - background: $white-light; - } - - .visibility-levels { - .controls { - margin-bottom: 9px; - } - - i { - color: inherit; - } - } -} - -.broadcast-messages { - .message { - line-height: 2; - } -} - -.broadcast-message { - @extend .alert-warning; - padding: 10px; - text-align: center; - - > div, - p { - display: inline; - margin: 0; - - a { - color: inherit; - text-decoration: underline; - } - } -} - -.broadcast-message-preview { - @extend .broadcast-message; - margin-bottom: 20px; -} - -// Users List - -.users-list { - .user-row { - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - white-space: nowrap; - } - - .user-details { - flex: 1 1 auto; - overflow: hidden; - padding-right: 8px; - } - - .user-name { - display: inline-block; - font-weight: 600; - } - - .user-name, - .user-email { - overflow: hidden; - text-overflow: ellipsis; - } - - .dropdown { - .btn-block { - margin-bottom: 0; - line-height: inherit; - } - } - - .label-default { - color: $btn-transparent-color; - } -} - -.abuse-reports { - .table { - table-layout: fixed; - } - - .subheading { - padding-bottom: $gl-padding; - } - - .message { - word-wrap: break-word; - } - - .btn { - white-space: normal; - padding: $gl-btn-padding; - } - - th { - width: 15%; - - &.wide { - width: 55%; - } - } - - @media (max-width: $screen-sm-max) { - th { - width: 100%; - } - - td { - width: 100%; - float: left; - } - } - - .no-reports { - .emoji-icon { - margin-left: $btn-side-margin; - margin-top: 3px; - } - - span { - font-size: 18px; - } - } -} - -.admin-builds-table { - .ci-table td:last-child { - min-width: 120px; - } -} diff --git a/app/assets/stylesheets/pages/confirmation.scss b/app/assets/stylesheets/pages/confirmation.scss deleted file mode 100644 index 8aab5e8231d..00000000000 --- a/app/assets/stylesheets/pages/confirmation.scss +++ /dev/null @@ -1,32 +0,0 @@ -.well-confirmation { - margin-bottom: 20px; - border-bottom: 1px solid $gray-darker; - - > h1, - h2, - h3, - h4, - h5, - h6 { - font-weight: 400; - } - - .lead { - margin-bottom: 20px; - } - - ul, - ol { - padding-left: 0; - } - - li { - list-style-type: none; - } -} - -.confirmation-content { - a { - color: $md-link-color; - } -} diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss deleted file mode 100644 index 4421ed6a0b9..00000000000 --- a/app/assets/stylesheets/pages/dashboard.scss +++ /dev/null @@ -1,43 +0,0 @@ -.dashboard { - .side { - .panel { - .panel-heading { - background: $background-color; - border-top-left-radius: 0; - } - - border-top-left-radius: 0; - } - } -} - -.dashboard-search-filter { - padding: 5px; - - .search-text-input { - float: left; - @extend .col-md-2; - } - - .btn { - margin-left: 5px; - float: left; - } -} - -.project-access-icon { - margin-left: 10px; - float: left; - margin-right: 15px; - margin-bottom: 15px; - - i { - color: $dashboard-project-access-icon-color; - } -} - -.dash-project-access-icon { - float: left; - margin-right: 5px; - width: 16px; -} diff --git a/app/assets/stylesheets/pages/errors.scss b/app/assets/stylesheets/pages/errors.scss deleted file mode 100644 index 11309817d31..00000000000 --- a/app/assets/stylesheets/pages/errors.scss +++ /dev/null @@ -1,16 +0,0 @@ -.error-page { - max-width: 400px; - margin: 0 auto; - - h1, - h2, - h3 { - text-align: center; - } - - h1 { - font-size: 56px; - line-height: 100px; - font-weight: 300; - } -} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index dc5945e1139..6234779ac19 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -129,6 +129,11 @@ margin-bottom: 4px; } + .btn-grouped { + float: none; + margin-right: 0; + } + .accept-action { width: 100%; text-align: center; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 0027d2caf22..08062b85504 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -280,6 +280,12 @@ } } +.admin-builds-table { + .ci-table td:last-child { + min-width: 120px; + } +} + // Pipeline visualization .toggle-pipeline-btn { diff --git a/app/assets/stylesheets/pages/tags.scss b/app/assets/stylesheets/pages/tags.scss deleted file mode 100644 index 24ebd3e7cfa..00000000000 --- a/app/assets/stylesheets/pages/tags.scss +++ /dev/null @@ -1,7 +0,0 @@ -.tag-buttons { - line-height: 40px; - - .btn:not(.dropdown-toggle) { - margin-left: 10px; - } -} diff --git a/app/assets/stylesheets/pages/votes.scss b/app/assets/stylesheets/pages/votes.scss deleted file mode 100644 index dc9a7d71e8b..00000000000 --- a/app/assets/stylesheets/pages/votes.scss +++ /dev/null @@ -1,4 +0,0 @@ -.votes-inline { - display: inline-block; - margin: 0 8px; -} diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb index 6546a07b41c..fdb05bb3228 100644 --- a/app/controllers/concerns/merge_requests_action.rb +++ b/app/controllers/concerns/merge_requests_action.rb @@ -6,7 +6,12 @@ module MergeRequestsAction @label = merge_requests_finder.labels.first @merge_requests = merge_requests_collection - .non_archived .page(params[:page]) end + + private + + def filter_params + super.merge(non_archived: true) + end end diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb index d174e1145a7..148e39630e3 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -5,9 +5,7 @@ class Projects::DiscussionsController < Projects::ApplicationController before_action :authorize_resolve_discussion! def resolve - discussion.resolve!(current_user) - - MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request) + Discussions::ResolveService.new(project, current_user, merge_request: merge_request).execute(discussion) render json: { resolved_by: discussion.resolved_by.try(:name), diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 4aea7bb62c4..4f66e01e0f7 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -46,8 +46,9 @@ class Projects::IssuesController < Projects::ApplicationController params[:issue] ||= ActionController::Parameters.new( assignee_id: "" ) + build_params = issue_params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions) + @issue = @noteable = Issues::BuildService.new(project, current_user, build_params).execute - @issue = @noteable = @project.issues.new(issue_params) respond_with(@issue) end @@ -75,7 +76,9 @@ class Projects::IssuesController < Projects::ApplicationController end def create - @issue = Issues::CreateService.new(project, current_user, issue_params.merge(request: request)).execute + extra_params = { request: request, + merge_request_for_resolving_discussions: merge_request_for_resolving_discussions } + @issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute respond_to do |format| format.html do @@ -169,6 +172,14 @@ class Projects::IssuesController < Projects::ApplicationController alias_method :awardable, :issue alias_method :spammable, :issue + def merge_request_for_resolving_discussions + return unless merge_request_iid = params[:merge_request_for_resolving_discussions] + + @merge_request_for_resolving_discussions ||= MergeRequestsFinder.new(current_user, project_id: project.id). + execute. + find_by(iid: merge_request_iid) + end + def authorize_read_issue! return render_404 unless can?(current_user, :read_issue, @issue) end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 001c83ccb4b..c9bee01b9ad 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -7,7 +7,7 @@ # current_user - which user use # params: # scope: 'created-by-me' or 'assigned-to-me' or 'all' -# state: 'open' or 'closed' or 'all' +# state: 'opened' or 'closed' or 'all' # group_id: integer # project_id: integer # milestone_title: string @@ -15,6 +15,7 @@ # search: string # label_name: string # sort: string +# non_archived: boolean # class IssuableFinder NONE = '0' @@ -38,6 +39,7 @@ class IssuableFinder items = by_author(items) items = by_label(items) items = by_due_date(items) + items = by_non_archived(items) sort(items) end @@ -207,10 +209,13 @@ class IssuableFinder end def by_state(items) - params[:state] ||= 'all' - - if items.respond_to?(params[:state]) - items.public_send(params[:state]) + case params[:state].to_s + when 'closed' + items.closed + when 'merged' + items.respond_to?(:merged) ? items.merged : items.closed + when 'opened' + items.opened else items end @@ -353,6 +358,10 @@ class IssuableFinder end end + def by_non_archived(items) + params[:non_archived].present? ? items.non_archived : items + end + def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' end diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 3b254e7d9d5..8b82255445e 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -14,6 +14,7 @@ # search: string # label_name: string # sort: string +# non_archived: boolean # class MergeRequestsFinder < IssuableFinder def klass diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index abcf84b4d15..8e19752a8a1 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -5,8 +5,9 @@ module CiStatusHelper end def ci_status_with_icon(status, target = nil) - content = ci_icon_for_status(status) + ci_label_for_status(status) + content = ci_icon_for_status(status) + ci_text_for_status(status) klass = "ci-status ci-#{status}" + if target link_to content, target, class: klass else @@ -14,7 +15,19 @@ module CiStatusHelper end end + def ci_text_for_status(status) + if detailed_status?(status) + status.text + else + status + end + end + def ci_label_for_status(status) + if detailed_status?(status) + return status.label + end + case status when 'success' 'passed' @@ -31,6 +44,10 @@ module CiStatusHelper end def ci_icon_for_status(status) + if detailed_status?(status) + return custom_icon(status.icon) + end + icon_name = case status when 'success' @@ -94,4 +111,10 @@ module CiStatusHelper class: klass, title: title, data: data end end + + def detailed_status?(status) + status.respond_to?(:text) && + status.respond_to?(:label) && + status.respond_to?(:icon) + end end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 0772d848289..eb435cc1783 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -174,7 +174,7 @@ module GitlabMarkdownHelper # Returns a String def cross_project_reference(project, entity) if entity.respond_to?(:to_reference) - "#{project.to_reference}#{entity.to_reference}" + entity.to_reference(project) else '' end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 4f180456b16..e5b1e6e8bc7 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -82,12 +82,6 @@ module LabelsHelper span.html_safe end - def render_colored_cross_project_label(label, source_project = nil, tooltip: true) - label_suffix = source_project ? source_project.name_with_namespace : label.project.name_with_namespace - label_suffix = " <i>in #{escape_once(label_suffix)}</i>" - render_colored_label(label, label_suffix, tooltip: tooltip) - end - def suggested_colors [ '#0033CC', @@ -166,6 +160,5 @@ module LabelsHelper end # Required for Banzai::Filter::LabelReferenceFilter - module_function :render_colored_label, :render_colored_cross_project_label, - :text_color_for_bg, :escape_once + module_function :render_colored_label, :text_color_for_bg, :escape_once end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index fabbf97d4db..caf6908505e 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -320,6 +320,10 @@ module Ci .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id } end + def detailed_status + Gitlab::Ci::Status::Pipeline::Factory.new(self).fabricate! + end + private def pipeline_data diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 94d9e2b3208..2c8698d8b5d 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -4,10 +4,10 @@ module Ci belongs_to :project, foreign_key: :gl_project_id - validates_uniqueness_of :key, scope: :gl_project_id validates :key, presence: true, - length: { within: 0..255 }, + uniqueness: { scope: :gl_project_id }, + length: { maximum: 255 }, format: { with: /\A[a-zA-Z0-9_]+\z/, message: "can contain only letters, digits and '_'." } diff --git a/app/models/commit.rb b/app/models/commit.rb index 176c524cf7b..248140f421b 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -92,19 +92,11 @@ class Commit end def to_reference(from_project = nil) - if cross_project_reference?(from_project) - project.to_reference + self.class.reference_prefix + self.id - else - self.id - end + commit_reference(from_project, id) end def reference_link_text(from_project = nil) - if cross_project_reference?(from_project) - project.to_reference + self.class.reference_prefix + self.short_id - else - self.short_id - end + commit_reference(from_project, short_id) end def diff_line_count @@ -329,6 +321,16 @@ class Commit private + def commit_reference(from_project, referable_commit_id) + reference = project.to_reference(from_project) + + if reference.present? + "#{reference}#{self.class.reference_prefix}#{referable_commit_id}" + else + referable_commit_id + end + end + def find_author_by_any_email User.find_by_any_email(author_email.downcase) end diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index ac2477fd973..d9af7f6c139 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -90,21 +90,24 @@ class CommitRange alias_method :id, :to_s def to_reference(from_project = nil) - if cross_project_reference?(from_project) - project.to_reference + self.class.reference_prefix + self.id + project_reference = project.to_reference(from_project) + + if project_reference.present? + project_reference + self.class.reference_prefix + self.id else self.id end end def reference_link_text(from_project = nil) - reference = ref_from + notation + ref_to + project_reference = project.to_reference(from_project) + reference = ref_from + notation + ref_to - if cross_project_reference?(from_project) - reference = project.to_reference + self.class.reference_prefix + reference + if project_reference.present? + project_reference + self.class.reference_prefix + reference + else + reference end - - reference end # Return a Hash of parameters for passing to a URL helper diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 69d8afc45da..0ea7b1b1098 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -41,7 +41,7 @@ module Issuable has_one :metrics validates :author, presence: true - validates :title, presence: true, length: { within: 0..255 } + validates :title, presence: true, length: { maximum: 255 } scope :authored, ->(user) { where(author_id: user) } scope :assigned_to, ->(u) { where(assignee_id: u.id)} diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb index dee940a3f88..8ba009fe04f 100644 --- a/app/models/concerns/referable.rb +++ b/app/models/concerns/referable.rb @@ -72,17 +72,4 @@ module Referable }x end end - - private - - # Check if a reference is being done cross-project - # - # from_project - Refering Project object - def cross_project_reference?(from_project) - if self.is_a?(Project) - self != from_project - else - from_project && self.project && self.project != from_project - end - end end diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 75a85563235..bbe813db823 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -88,6 +88,10 @@ class Discussion @first_note ||= @notes.first end + def first_note_to_resolve + @first_note_to_resolve ||= notes.detect(&:to_be_resolved?) + end + def last_note @last_note ||= @notes.last end diff --git a/app/models/environment.rb b/app/models/environment.rb index a7f4156fc2e..96700143ddd 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -9,7 +9,7 @@ class Environment < ActiveRecord::Base validates :name, presence: true, uniqueness: { scope: :project_id }, - length: { within: 0..255 }, + length: { maximum: 255 }, format: { with: Gitlab::Regex.environment_name_regex, message: Gitlab::Regex.environment_name_regex_message } diff --git a/app/models/issue.rb b/app/models/issue.rb index fbf07040301..7fe92051037 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -153,11 +153,7 @@ class Issue < ActiveRecord::Base def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" - if cross_project_reference?(from_project) - reference = project.to_reference + reference - end - - reference + "#{project.to_reference(from_project)}#{reference}" end def referenced_merge_requests(current_user = nil) diff --git a/app/models/key.rb b/app/models/key.rb index ff8dda2dc89..a5d25409730 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -8,10 +8,18 @@ class Key < ActiveRecord::Base before_validation :generate_fingerprint - validates :title, presence: true, length: { within: 0..255 } - validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ } - validates :key, format: { without: /\n|\r/, message: 'should be a single line' } - validates :fingerprint, uniqueness: true, presence: { message: 'cannot be generated' } + validates :title, + presence: true, + length: { maximum: 255 } + validates :key, + presence: true, + length: { maximum: 5000 }, + format: { with: /\A(ssh|ecdsa)-.*\Z/ } + validates :key, + format: { without: /\n|\r/, message: 'should be a single line' } + validates :fingerprint, + uniqueness: true, + presence: { message: 'cannot be generated' } delegate :name, :email, to: :user, prefix: true diff --git a/app/models/label.rb b/app/models/label.rb index d9287f2dc29..d38c37344c9 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -144,9 +144,10 @@ class Label < ActiveRecord::Base # # Examples: # - # Label.first.to_reference # => "~1" - # Label.first.to_reference(format: :name) # => "~\"bug\"" - # Label.first.to_reference(project1, project2) # => "gitlab-org/gitlab-ce~1" + # Label.first.to_reference # => "~1" + # Label.first.to_reference(format: :name) # => "~\"bug\"" + # Label.first.to_reference(project, same_namespace_project) # => "gitlab-ce~1" + # Label.first.to_reference(project, another_namespace_project) # => "gitlab-org/gitlab-ce~1" # # Returns a String # @@ -154,8 +155,8 @@ class Label < ActiveRecord::Base format_reference = label_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" - if cross_project_reference?(source_project, target_project) - source_project.to_reference + reference + if source_project + "#{source_project.to_reference(target_project)}#{reference}" else reference end @@ -169,10 +170,6 @@ class Label < ActiveRecord::Base private - def cross_project_reference?(source_project, target_project) - source_project && target_project && source_project != target_project - end - def issues_count(user, params = {}) params.merge!(subject_foreign_key => subject.id, label_name: title, scope: 'all') IssuesFinder.new(user, params.with_indifferent_access).execute.count diff --git a/app/models/member.rb b/app/models/member.rb index df93aaee847..3b65587c66b 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -63,6 +63,7 @@ class Member < ActiveRecord::Base after_create :send_request, if: :request?, unless: :importing? after_create :create_notification_setting, unless: [:pending?, :importing?] after_create :post_create_hook, unless: [:pending?, :importing?] + after_create :refresh_member_authorized_projects, if: :importing? after_update :post_update_hook, unless: [:pending?, :importing?] after_destroy :post_destroy_hook, unless: :pending? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index bfb016df46d..33b578e12c1 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -176,11 +176,7 @@ class MergeRequest < ActiveRecord::Base def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" - if cross_project_reference?(from_project) - reference = project.to_reference + reference - end - - reference + "#{project.to_reference(from_project)}#{reference}" end def first_commit @@ -480,6 +476,14 @@ class MergeRequest < ActiveRecord::Base @diff_discussions ||= self.notes.diff_notes.discussions end + def resolvable_discussions + @resolvable_discussions ||= diff_discussions.select(&:to_be_resolved?) + end + + def discussions_can_be_resolved_by?(user) + resolvable_discussions.all? { |discussion| discussion.can_resolve?(user) } + end + def find_diff_discussion(discussion_id) notes = self.notes.diff_notes.where(discussion_id: discussion_id).fresh.to_a return if notes.empty? diff --git a/app/models/milestone.rb b/app/models/milestone.rb index c774e69080c..45ca97adad1 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -113,19 +113,16 @@ class Milestone < ActiveRecord::Base # # Examples: # - # Milestone.first.to_reference # => "%1" - # Milestone.first.to_reference(format: :name) # => "%\"goal\"" - # Milestone.first.to_reference(project) # => "gitlab-org/gitlab-ce%1" + # Milestone.first.to_reference # => "%1" + # Milestone.first.to_reference(format: :name) # => "%\"goal\"" + # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1" + # Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1" # def to_reference(from_project = nil, format: :iid) format_reference = milestone_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" - if cross_project_reference?(from_project) - project.to_reference + reference - else - reference - end + "#{project.to_reference(from_project)}#{reference}" end def reference_link_text(from_project = nil) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 891dffac648..7a545f752b6 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -12,17 +12,17 @@ class Namespace < ActiveRecord::Base validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :name, - length: { within: 0..255 }, - namespace_name: true, presence: true, - uniqueness: true + uniqueness: true, + length: { maximum: 255 }, + namespace_name: true - validates :description, length: { within: 0..255 } + validates :description, length: { maximum: 255 } validates :path, - length: { within: 1..255 }, - namespace: true, presence: true, - uniqueness: { case_sensitive: false } + uniqueness: { case_sensitive: false }, + length: { maximum: 255 }, + namespace: true delegate :name, to: :owner, allow_nil: true, prefix: true diff --git a/app/models/note.rb b/app/models/note.rb index 5b50ca285c3..08bd08743ef 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -99,7 +99,7 @@ class Note < ActiveRecord::Base end def discussions - Discussion.for_notes(all) + Discussion.for_notes(fresh) end def grouped_diff_discussions diff --git a/app/models/project.rb b/app/models/project.rb index f01cb613b85..590885c0177 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -172,13 +172,13 @@ class Project < ActiveRecord::Base validates :description, length: { maximum: 2000 }, allow_blank: true validates :name, presence: true, - length: { within: 0..255 }, + length: { maximum: 255 }, format: { with: Gitlab::Regex.project_name_regex, message: Gitlab::Regex.project_name_regex_message } validates :path, presence: true, project_path: true, - length: { within: 0..255 }, + length: { maximum: 255 }, format: { with: Gitlab::Regex.project_path_regex, message: Gitlab::Regex.project_path_regex_message } validates :namespace, presence: true @@ -419,7 +419,11 @@ class Project < ActiveRecord::Base def reference_pattern name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR - %r{(?<project>#{name_pattern}/#{name_pattern})} + + %r{ + ((?<namespace>#{name_pattern})\/)? + (?<project>#{name_pattern}) + }x end def trending @@ -650,8 +654,20 @@ class Project < ActiveRecord::Base end end - def to_reference(_from_project = nil) - path_with_namespace + def to_reference(from_project = nil) + if cross_namespace_reference?(from_project) + path_with_namespace + elsif cross_project_reference?(from_project) + path + end + end + + def to_human_reference(from_project = nil) + if cross_namespace_reference?(from_project) + name_with_namespace + elsif cross_project_reference?(from_project) + name + end end def web_url @@ -1327,10 +1343,21 @@ class Project < ActiveRecord::Base private + # Check if a reference is being done cross-project + # + # from_project - Refering Project object + def cross_project_reference?(from_project) + from_project && self != from_project + end + def pushes_since_gc_redis_key "projects/#{id}/pushes_since_gc" end + def cross_namespace_reference?(from_project) + from_project && namespace != from_project.namespace + end + def default_branch_protected? current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE diff --git a/app/models/repository.rb b/app/models/repository.rb index e2e7d08abac..3c4b0212af7 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -85,11 +85,7 @@ class Repository # This method return true if repository contains some content visible in project page. # def has_visible_content? - return @has_visible_content unless @has_visible_content.nil? - - @has_visible_content = cache.fetch(:has_visible_content?) do - branch_count > 0 - end + branch_count > 0 end def commit(ref = 'HEAD') @@ -374,12 +370,6 @@ class Repository return unless empty? expire_method_caches(%i(empty?)) - expire_has_visible_content_cache - end - - def expire_has_visible_content_cache - cache.expire(:has_visible_content?) - @has_visible_content = nil end def lookup_cache @@ -467,7 +457,6 @@ class Repository # Runs code after a new branch has been created. def after_create_branch expire_branches_cache - expire_has_visible_content_cache repository_event(:push_branch) end @@ -481,7 +470,6 @@ class Repository # Runs code after an existing branch has been removed. def after_remove_branch - expire_has_visible_content_cache expire_branches_cache end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 8ff4e7ae718..98ccf5f331f 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -27,9 +27,9 @@ class Snippet < ActiveRecord::Base delegate :name, :email, to: :author, prefix: true, allow_nil: true validates :author, presence: true - validates :title, presence: true, length: { within: 0..255 } + validates :title, presence: true, length: { maximum: 255 } validates :file_name, - length: { within: 0..255 }, + length: { maximum: 255 }, format: { with: Gitlab::Regex.file_name_regex, message: Gitlab::Regex.file_name_regex_message } @@ -67,11 +67,11 @@ class Snippet < ActiveRecord::Base def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{id}" - if cross_project_reference?(from_project) - reference = project.to_reference + reference + if project.present? + "#{project.to_reference(from_project)}#{reference}" + else + reference end - - reference end def self.content_types @@ -94,6 +94,10 @@ class Snippet < ActiveRecord::Base 0 end + def file_name + super.to_s + end + # alias for compatibility with blobs and highlighting def path file_name diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 8b25332b73c..7b1752df0e1 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -1,6 +1,8 @@ module Ci class BuildPolicy < CommitStatusPolicy def rules + can! :read_build if @subject.project.public_builds? + super # If we can't read build we should also not have that diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 8ac4bd9df6d..d5aadfce76a 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -12,9 +12,6 @@ class ProjectPolicy < BasePolicy guest_access! public_access! - # Allow to read builds for internal projects - can! :read_build if project.public_builds? - if project.request_access_enabled && !(owner || user.admin? || project.team.member?(user) || project_group_member?(user)) can! :request_access @@ -46,6 +43,11 @@ class ProjectPolicy < BasePolicy can! :create_note can! :upload_file can! :read_cycle_analytics + + if project.public_builds? + can! :read_pipeline + can! :read_build + end end def reporter_access! diff --git a/app/services/discussions/base_service.rb b/app/services/discussions/base_service.rb new file mode 100644 index 00000000000..e4dfe6e71bb --- /dev/null +++ b/app/services/discussions/base_service.rb @@ -0,0 +1,4 @@ +module Discussions + class BaseService < ::BaseService + end +end diff --git a/app/services/discussions/resolve_service.rb b/app/services/discussions/resolve_service.rb new file mode 100644 index 00000000000..0437195f588 --- /dev/null +++ b/app/services/discussions/resolve_service.rb @@ -0,0 +1,24 @@ +module Discussions + class ResolveService < Discussions::BaseService + def execute(one_or_more_discussions) + Array(one_or_more_discussions).each { |discussion| resolve_discussion(discussion) } + end + + def resolve_discussion(discussion) + return unless discussion.can_resolve?(current_user) + + discussion.resolve!(current_user) + + MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request) + SystemNoteService.discussion_continued_in_issue(discussion, project, current_user, follow_up_issue) if follow_up_issue + end + + def merge_request + params[:merge_request] + end + + def follow_up_issue + params[:follow_up_issue] + end + end +end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index ce68e433ab8..b5f63cc5a1a 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -120,9 +120,10 @@ class IssuableBaseService < BaseService def merge_slash_commands_into_params!(issuable) description, command_params = SlashCommands::InterpretService.new(project, current_user). - execute(params[:description], issuable) + execute(params[:description], issuable) - params[:description] = description + # Avoid a description already set on an issuable to be overwritten by a nil + params[:description] = description if params.has_key?(:description) params.merge!(command_params) end diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 9ea3ce084ba..742e834df97 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -1,5 +1,13 @@ module Issues class BaseService < ::IssuableBaseService + attr_reader :merge_request_for_resolving_discussions + + def initialize(*args) + super + + @merge_request_for_resolving_discussions ||= params.delete(:merge_request_for_resolving_discussions) + end + def hook_data(issue, action) issue_data = issue.to_hook_data(current_user) issue_url = Gitlab::UrlBuilder.build(issue) diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb new file mode 100644 index 00000000000..a63982f60c8 --- /dev/null +++ b/app/services/issues/build_service.rb @@ -0,0 +1,50 @@ +module Issues + class BuildService < Issues::BaseService + def execute + @issue = project.issues.new(issue_params) + end + + def issue_params_with_info_from_merge_request + return {} unless merge_request_for_resolving_discussions + + { title: title_from_merge_request, description: description_from_merge_request } + end + + def title_from_merge_request + "Follow-up from \"#{merge_request_for_resolving_discussions.title}\"" + end + + def description_from_merge_request + if merge_request_for_resolving_discussions.resolvable_discussions.empty? + return "There are no unresolved discussions. "\ + "Review the conversation in #{merge_request_for_resolving_discussions.to_reference}" + end + + description = "The following discussions from #{merge_request_for_resolving_discussions.to_reference} should be addressed:" + [description, *items_for_discussions].join("\n\n") + end + + def items_for_discussions + merge_request_for_resolving_discussions.resolvable_discussions.map { |discussion| item_for_discussion(discussion) } + end + + def item_for_discussion(discussion) + first_note = discussion.first_note_to_resolve + other_note_count = discussion.notes.size - 1 + creation_time = first_note.created_at.to_s(:medium) + note_url = Gitlab::UrlBuilder.build(first_note) + + discussion_info = "- [ ] #{first_note.author.to_reference} commented in a discussion on [#{creation_time}](#{note_url}): " + discussion_info << " (+#{other_note_count} #{'comment'.pluralize(other_note_count)})" if other_note_count > 0 + + note_without_block_quotes = Banzai::Filter::BlockquoteFenceFilter.new(first_note.note).call + quote = ">>>\n#{note_without_block_quotes}\n>>>" + + [discussion_info, quote].join("\n\n") + end + + def issue_params + @issue_params ||= issue_params_with_info_from_merge_request.merge(params.slice(:title, :description)) + end + end +end diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index ea1690f3e38..d2eb46ac41b 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -4,7 +4,8 @@ module Issues @request = params.delete(:request) @api = params.delete(:api) - @issue = project.issues.new + issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions) + @issue = BuildService.new(project, current_user, issue_attributes).execute create(@issue) end @@ -18,6 +19,17 @@ module Issues notification_service.new_issue(issuable, current_user) todo_service.new_issue(issuable, current_user) user_agent_detail_service.create + + if merge_request_for_resolving_discussions.try(:discussions_can_be_resolved_by?, current_user) + resolve_discussions_in_merge_request(issuable) + end + end + + def resolve_discussions_in_merge_request(issue) + Discussions::ResolveService.new(project, current_user, + merge_request: merge_request_for_resolving_discussions, + follow_up_issue: issue). + execute(merge_request_for_resolving_discussions.resolvable_discussions) end private diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 3cf6467804f..8b48d90f60b 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -163,6 +163,14 @@ module SystemNoteService create_note(noteable: merge_request, project: project, author: author, note: body) end + def discussion_continued_in_issue(discussion, project, author, issue) + body = "Added #{issue.to_reference} to continue this discussion" + note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body) + note_attributes[:type] = note_attributes.delete(:note_type) + + create_note(note_attributes) + end + # Called when the title of a Noteable is changed # # noteable - Noteable object that responds to `title` diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml index 7bbc75db9ff..c4b748d0ab8 100644 --- a/app/views/admin/abuse_reports/index.html.haml +++ b/app/views/admin/abuse_reports/index.html.haml @@ -4,7 +4,7 @@ .abuse-reports - if @abuse_reports.present? .table-holder - %table.table + %table.table.responsive-table %thead.hidden-sm.hidden-xs %tr %th User @@ -13,8 +13,6 @@ %th Action = render @abuse_reports - else - .no-reports - %span.pull-left - There are no abuse reports! - .pull-left - = emoji_icon 'tada' + .empty-state + .text-center + %h4 There are no abuse reports! #{emoji_icon 'tada'} diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 1db2150f336..e51f4ac1d93 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -113,7 +113,7 @@ %hr .row .col-sm-4 - .light-well + .light-well.well-centered %h4 Projects .data = link_to admin_namespaces_projects_path do @@ -121,7 +121,7 @@ %hr = link_to('New Project', new_project_path, class: "btn btn-new") .col-sm-4 - .light-well + .light-well.well-centered %h4 Users .data = link_to admin_users_path do @@ -129,7 +129,7 @@ %hr = link_to 'New User', new_admin_user_path, class: "btn btn-new" .col-sm-4 - .light-well + .light-well.well-centered %h4 Groups .data = link_to admin_groups_path do @@ -143,7 +143,7 @@ %hr - @projects.each do |project| %p - = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated' + = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60' %span.light.pull-right #{time_ago_with_tooltip(project.created_at)} @@ -152,7 +152,7 @@ %hr - @users.each do |user| %p - = link_to [:admin, user], class: 'str-truncated' do + = link_to [:admin, user], class: 'str-truncated-60' do = user.name %span.light.pull-right #{time_ago_with_tooltip(user.created_at)} @@ -162,7 +162,7 @@ %hr - @groups.each do |group| %p - = link_to [:admin, group], class: 'str-truncated' do + = link_to [:admin, group], class: 'str-truncated-60' do = group.name %span.light.pull-right #{time_ago_with_tooltip(group.created_at)} diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml index 4bf1c9cde3c..2d9588f9d27 100644 --- a/app/views/admin/users/_user.html.haml +++ b/app/views/admin/users/_user.html.haml @@ -1,8 +1,8 @@ -%li.user-row +%li.flex-row .user-avatar = image_tag avatar_icon(user), class: "avatar", alt: '' - .user-details - .user-name + .row-main-content + .user-name.row-title.str-truncated-100 = link_to user.name, [:admin, user] - if user.blocked? %span.label.label-danger blocked @@ -12,7 +12,7 @@ %span.label.label-default External - if user == current_user %span It's you! - .user-email + .row-second-line.str-truncated-100 = mail_to user.email, user.email .controls = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn' diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index d3038ae644f..4dc44225d49 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -68,7 +68,7 @@ %small.badge= number_with_delimiter(User.without_projects.count) .fade-right - %ul.users-list.content-list + %ul.flex-list.content-list - if @users.empty? %li .nothing-here-block No users found. diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml index 20cd7b0179d..fb70d158096 100644 --- a/app/views/devise/confirmations/almost_there.haml +++ b/app/views/devise/confirmations/almost_there.haml @@ -1,12 +1,13 @@ -.well-confirmation.text-center +.well-confirmation.text-center.append-bottom-20 %h1.prepend-top-0 Almost there... - %p.lead + %p.lead.append-bottom-20 Please check your email to confirm your account + %hr - if current_application_settings.after_sign_up_text.present? .well-confirmation.text-center = markdown_field(current_application_settings, :after_sign_up_text) -%p.confirmation-content.text-center +%p.text-center No confirmation email received? Please check your spam folder or .append-bottom-20.prepend-top-20.text-center %a.btn.btn-lg.btn-success{ href: new_user_confirmation_path } diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index f7edb47b666..f3539fd372d 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -31,7 +31,7 @@ = link_to merge_requests_group_path(@group), title: 'Merge Requests' do %span Merge Requests - - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute + - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute %span.badge.count= number_with_delimiter(merge_requests.count) = nav_link(controller: [:group_members]) do = link_to group_group_members_path(@group), title: 'Members' do diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 7b995bd8735..40bfa01a45a 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,42 +1,41 @@ - if !project.empty_repo? && can?(current_user, :download_code, project) - %span{class: 'download-button'} - .dropdown.inline - %button.btn{ 'data-toggle' => 'dropdown' } - = icon('download') - = icon("caret-down") - %span.sr-only - Select Archive Format - %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } - %li.dropdown-header Source code - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do - %i.fa.fa-download - %span Download zip - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.gz - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.bz2 - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar + .dropdown.inline.download-button + %button.btn{ 'data-toggle' => 'dropdown' } + = icon('download') + = icon("caret-down") + %span.sr-only + Select Archive Format + %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } + %li.dropdown-header Source code + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do + %i.fa.fa-download + %span Download zip + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.gz + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.bz2 + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar - - pipeline = project.pipelines.latest_successful_for(ref) - - if pipeline - - artifacts = pipeline.builds.latest.with_artifacts - - if artifacts.any? - %li.dropdown-header Artifacts - - unless pipeline.latest? - - latest_pipeline = project.pipeline_for(ref) - %li - .unclickable= ci_status_for_statuseable(latest_pipeline) - %li.dropdown-header Previous Artifacts - - artifacts.each do |job| - %li - = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do - %i.fa.fa-download - %span Download '#{job.name}' + - pipeline = project.pipelines.latest_successful_for(ref) + - if pipeline + - artifacts = pipeline.builds.latest.with_artifacts + - if artifacts.any? + %li.dropdown-header Artifacts + - unless pipeline.latest? + - latest_pipeline = project.pipeline_for(ref) + %li + .unclickable= ci_status_for_statuseable(latest_pipeline) + %li.dropdown-header Previous Artifacts + - artifacts.each do |job| + %li + = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do + %i.fa.fa-download + %span Download '#{job.name}' diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 4c7b14a04db..0f08f4e8592 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -1,12 +1,13 @@ - status = pipeline.status +- detailed_status = pipeline.detailed_status - show_commit = local_assigns.fetch(:show_commit, true) - show_branch = local_assigns.fetch(:show_branch, true) %tr.commit %td.commit-link - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "ci-status ci-#{status}" do - = ci_icon_for_status(status) - = ci_label_for_status(status) + = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "ci-status ci-#{detailed_status}" do + = ci_icon_for_status(detailed_status) + = ci_text_for_status(detailed_status) %td = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 6d9b91ad0e7..9ab7971b56c 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -8,14 +8,13 @@ = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline' = ci_label_for_status(status) for - - commit = @merge_request.diff_head_commit = succeed "." do = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" %span.ci-coverage - elsif @merge_request.has_ci? - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX - - # Remove in later versions when services like Jenkins will set CI status via Commit status API + - # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API .mr-widget-heading - %w[success skipped canceled failed running pending].each do |status| .ci_widget{class: "ci-#{status}", style: "display:none"} diff --git a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml index 35d5677ee37..e094f97f3b6 100644 --- a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml +++ b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml @@ -3,4 +3,8 @@ This merge request has unresolved discussions %p - Please resolve these discussions to allow this merge request to be merged.
\ No newline at end of file + Please resolve these discussions + - if @project.issues_enabled? && can?(current_user, :create_issue, @project) + or + = link_to "open an issue to resolve them later", new_namespace_project_issue_path(@project.namespace, @project, merge_request_for_resolving_discussions: @merge_request.iid) + to allow this merge request to be merged. diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 095bd254d6b..229bdfb0e8d 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,6 +1,6 @@ .page-content-header .header-main-content - = ci_status_with_icon(@pipeline.status) + = ci_status_with_icon(@pipeline.detailed_status) %strong Pipeline ##{@commit.pipelines.last.id} triggered #{time_ago_with_tooltip(@commit.authored_date)} by = author_avatar(@commit, size: 24) diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml index 96221a20502..1f698558bce 100644 --- a/app/views/projects/pipelines_settings/show.html.haml +++ b/app/views/projects/pipelines_settings/show.html.haml @@ -86,6 +86,9 @@ %li tap --coverage-report=text-summary (NodeJS) - %code ^Statements\s*:\s*([^%]+) + %li + excoveralls (Elixir) - + %code \[TOTAL\]\s+(\d+\.\d+)% = f.submit 'Save changes', class: "btn btn-save" diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 155af755759..12facb6eb73 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -3,8 +3,16 @@ = render "projects/commits/head" %div{ class: container_class } - .sub-header-block - .pull-right.tag-buttons + .top-area.multi-line + .nav-text + .title + %span.item-title= @tag.name + - if @commit + = render 'projects/branches/commit', commit: @commit, project: @project + - else + Cant find HEAD commit for this tag + + .nav-controls - if can?(current_user, :push_code, @project) = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Edit release notes' do = icon("pencil") @@ -15,15 +23,8 @@ = render 'projects/buttons/download', project: @project, ref: @tag.name - if can?(current_user, :admin_project, @project) .pull-right - = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do + = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do %i.fa.fa-trash-o - .tag-info.append-bottom-10 - .title - %span.item-title= @tag.name - - if @commit - = render 'projects/branches/commit', commit: @commit, project: @project - - else - Cant find HEAD commit for this tag - if @tag.message.present? %pre.body = strip_gpg_signature(@tag.message) diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 2f05093f435..bdb00bfa33c 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -42,6 +42,21 @@ = render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form +- if @merge_request_for_resolving_discussions + .form-group + .col-sm-10.col-sm-offset-2 + - if @merge_request_for_resolving_discussions.discussions_can_be_resolved_by?(current_user) + = icon('exclamation-triangle') + Creating this issue will mark all discussions in + = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions) + as resolved. + = hidden_field_tag 'merge_request_for_resolving_discussions', @merge_request_for_resolving_discussions.iid + - else + = icon('exclamation-triangle') + You can't automatically mark all discussions in + = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions) + as resolved. Ask someone with sufficient rights to resolve the them. + - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) .row-content-block{class: (is_footer ? "footer-block" : "middle-block")} - if issuable.new_record? diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index 5527a2f889a..0af92b59584 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -4,22 +4,22 @@ %ul.nav-links.issues-state-filters %li{class: ("active" if params[:state] == 'opened')} - = link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do + = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened." do #{issuables_state_counter_text(type, :opened)} - if type == :merge_requests %li{class: ("active" if params[:state] == 'merged')} - = link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do + = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.' do #{issuables_state_counter_text(type, :merged)} %li{class: ("active" if params[:state] == 'closed')} - = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do + = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.' do #{issuables_state_counter_text(type, :closed)} - else %li{class: ("active" if params[:state] == 'closed')} - = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do + = link_to page_filter_path(state: 'closed', label: true), id: 'state-all', title: 'Filter by issues that are currently closed.' do #{issuables_state_counter_text(type, :closed)} %li{class: ("active" if params[:state] == 'all')} - = link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do + = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}." do #{issuables_state_counter_text(type, :all)} diff --git a/changelogs/unreleased/23589-open-issue-for-mr.yml b/changelogs/unreleased/23589-open-issue-for-mr.yml new file mode 100644 index 00000000000..cea48b85254 --- /dev/null +++ b/changelogs/unreleased/23589-open-issue-for-mr.yml @@ -0,0 +1,5 @@ +--- +title: Resolve all discussions in a merge request by creating an issue collecting + them +merge_request: 7180 +author: Bob Van Landuyt diff --git a/changelogs/unreleased/24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml b/changelogs/unreleased/24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml deleted file mode 100644 index 01b19a47ecd..00000000000 --- a/changelogs/unreleased/24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Speed up issuable dashboards -merge_request: -author: diff --git a/changelogs/unreleased/24733-archived-project-merge-request-count.yml b/changelogs/unreleased/24733-archived-project-merge-request-count.yml new file mode 100644 index 00000000000..2bc7e91825a --- /dev/null +++ b/changelogs/unreleased/24733-archived-project-merge-request-count.yml @@ -0,0 +1,4 @@ +--- +title: Fix Archived project merge requests add to group's Merge Requests +merge_request: 7790 +author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml b/changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml deleted file mode 100644 index 58efd9113f2..00000000000 --- a/changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't change relative URLs to absolute URLs in the Help page -merge_request: -author: diff --git a/changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml b/changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml new file mode 100644 index 00000000000..7afc794866b --- /dev/null +++ b/changelogs/unreleased/25202-fix-mr-widget-content-wrapping.yml @@ -0,0 +1,5 @@ +--- +title: Centered Accept Merge Request button within MR widget and added padding for + viewports smaller than 768px +merge_request: +author: Ryan Harris diff --git a/changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml b/changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml new file mode 100644 index 00000000000..9f14463fdd1 --- /dev/null +++ b/changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml @@ -0,0 +1,4 @@ +--- +title: Authorize users into imported GitLab project +merge_request: +author: diff --git a/changelogs/unreleased/fix-github-branch-formatter.yml b/changelogs/unreleased/fix-github-branch-formatter.yml deleted file mode 100644 index c8698f507de..00000000000 --- a/changelogs/unreleased/fix-github-branch-formatter.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix branch validation for GitHub PR where repo/fork was renamed/deleted -merge_request: -author: diff --git a/changelogs/unreleased/glm-shorthand-reference.yml b/changelogs/unreleased/glm-shorthand-reference.yml new file mode 100644 index 00000000000..6d60f23c798 --- /dev/null +++ b/changelogs/unreleased/glm-shorthand-reference.yml @@ -0,0 +1,4 @@ +--- +title: Add shorthand support to gitlab markdown references +merge_request: 7255 +author: Oswaldo Ferreira diff --git a/changelogs/unreleased/process-commit-worker-improvements.yml b/changelogs/unreleased/process-commit-worker-improvements.yml deleted file mode 100644 index 0038c6e34e6..00000000000 --- a/changelogs/unreleased/process-commit-worker-improvements.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Pass commit data to ProcessCommitWorker to reduce Git overhead -merge_request: 7744 -author: diff --git a/changelogs/unreleased/remove-has-visible-content-caching.yml b/changelogs/unreleased/remove-has-visible-content-caching.yml new file mode 100644 index 00000000000..e2940c60443 --- /dev/null +++ b/changelogs/unreleased/remove-has-visible-content-caching.yml @@ -0,0 +1,4 @@ +--- +title: Remove visible content caching +merge_request: +author: diff --git a/changelogs/unreleased/validate-state-param-when-filtering-issuables.yml b/changelogs/unreleased/validate-state-param-when-filtering-issuables.yml new file mode 100644 index 00000000000..3fb025806b0 --- /dev/null +++ b/changelogs/unreleased/validate-state-param-when-filtering-issuables.yml @@ -0,0 +1,4 @@ +--- +title: Validate state param when filtering issuables +merge_request: +author: diff --git a/changelogs/unreleased/zj-guest-reads-public-builds.yml b/changelogs/unreleased/zj-guest-reads-public-builds.yml new file mode 100644 index 00000000000..1859addd606 --- /dev/null +++ b/changelogs/unreleased/zj-guest-reads-public-builds.yml @@ -0,0 +1,4 @@ +--- +title: Guests can read builds when public +merge_request: 6842 +author: diff --git a/config/routes/group.rb b/config/routes/group.rb index 9fe72990994..776c31c9dac 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -1,15 +1,5 @@ resources :groups, only: [:index, :new, :create] -scope(path: 'groups/*id', - controller: :groups, - constraints: { id: Gitlab::Regex.namespace_route_regex }) do - get :edit, as: :edit_group - get :issues, as: :issues_group - get :merge_requests, as: :merge_requests_group - get :projects, as: :projects_group - get :activity, as: :activity_group -end - scope(path: 'groups/*group_id', module: :groups, as: :group, @@ -22,10 +12,20 @@ scope(path: 'groups/*group_id', resource :avatar, only: [:destroy] resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] - resources :labels, except: [:show], constraints: { id: /\d+/ } do + resources :labels, except: [:show] do post :toggle_subscription, on: :member end end +scope(path: 'groups/*id', + controller: :groups, + constraints: { id: Gitlab::Regex.namespace_route_regex }) do + get :edit, as: :edit_group + get :issues, as: :issues_group + get :merge_requests, as: :merge_requests_group + get :projects, as: :projects_group + get :activity, as: :activity_group +end + # Must be last route in this file get 'groups/*id' => 'groups#show', as: :group_canonical, constraints: { id: Gitlab::Regex.namespace_route_regex } diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md index 0387d730489..06291705702 100644 --- a/doc/administration/custom_hooks.md +++ b/doc/administration/custom_hooks.md @@ -42,6 +42,25 @@ Follow the steps below to set up a custom hook: That's it! Assuming the hook code is properly implemented the hook will fire as appropriate. +## Chained hooks support + +> [Introduced][93] in GitLab Shell 4.1.0. + +The hooks could be also placed in `hooks/<hook_name>.d` (global) or `custom_hooks/<hook_name>.d` (per project) +directories supporting chained execution of the hooks. + +The hooks are searched and executed in this order: +1. `<project>.git/hooks/` - symlink to `gitlab-shell/hooks` global dir +1. `<project>.git/hooks/<hook_name>` - executed by `git` itself, this is `gitlab-shell/hooks/<hook_name>` +1. `<project>.git/custom_hooks/<hook_name>` - per project hook (this is already existing behavior) +1. `<project>.git/custom_hooks/<hook_name>.d/*` - per project hooks +1. `<project>.git/hooks/<hook_name>.d/*` - global hooks: all executable files (minus editor backup files) + +Files in `.d` directories need to be executable and not match the backup file pattern (`*~`). + +The hooks of the same type are executed in order and execution stops on the first +script exiting with non-zero value. + ## Custom error messages > [Introduced][5073] in GitLab 8.10. @@ -54,3 +73,4 @@ STDERR takes precedence over STDOUT. [hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks [5073]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5073 +[93]: https://gitlab.com/gitlab-org/gitlab-shell/merge_requests/93 diff --git a/doc/api/issues.md b/doc/api/issues.md index 16f8e32c82a..119125bcd3d 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -330,6 +330,7 @@ POST /projects/:id/issues | `labels` | string | no | Comma-separated label names for an issue | | `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) | | `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | +| `merge_request_for_resolving_discussions` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values. | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug @@ -506,7 +507,7 @@ Example response: ## Subscribe to an issue -Subscribes the authenticated user to an issue to receive notifications. +Subscribes the authenticated user to an issue to receive notifications. If the user is already subscribed to the issue, the status code `304` is returned. diff --git a/doc/api/projects.md b/doc/api/projects.md index 132be644b59..0bc2a5210aa 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -347,7 +347,8 @@ Parameters: ### Get single project Get a specific project, identified by project ID or NAMESPACE/PROJECT_NAME, which is owned by the authenticated user. -If using namespaced projects call make sure that the NAMESPACE/PROJECT_NAME is URL-encoded, eg. `/api/v3/projects/diaspora%2Fdiaspora` (where `/` is represented by `%2F`). +If using namespaced projects call make sure that the NAMESPACE/PROJECT_NAME is URL-encoded, eg. `/api/v3/projects/diaspora%2Fdiaspora` (where `/` is represented by `%2F`). This endpoint can be accessed without authentication if +the project is publicly accessible. ``` GET /projects/:id @@ -436,10 +437,47 @@ Parameters: } ``` +## Get project users + +Get the users list of a project. + + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `search` | string | no | Search for specific users | + +``` +GET /projects/:id/users +``` + +```json +[ + { + "id": 1, + "username": "john_smith", + "name": "John Smith", + "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", + "web_url": "http://localhost:3000/john_smith" + }, + { + "id": 2, + "username": "jack_smith", + "name": "Jack Smith", + "state": "blocked", + "avatar_url": "http://gravatar.com/../e32131cd8.jpeg", + "web_url": "http://localhost:3000/jack_smith" + } +] +``` + ### Get project events -Get the events for the specified project. -Sorted from newest to oldest +Get the events for the specified project sorted from newest to oldest. This +endpoint can be accessed without authentication if the project is publicly +accessible. ``` GET /projects/:id/events @@ -1344,7 +1382,9 @@ Parameter: ## Search for projects by name -Search for projects by name which are accessible to the authenticated user. +Search for projects by name which are accessible to the authenticated user. This +endpoint can be accessed without authentication if the project is publicly +accessible. ``` GET /projects/search/:query diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md index 175e9d79904..82ffb841729 100644 --- a/doc/ci/examples/php.md +++ b/doc/ci/examples/php.md @@ -40,7 +40,7 @@ repository with the following content: #!/bin/bash # We need to install dependencies only for Docker -[[ ! -e /.dockerenv ]] && [[ ! -e /.dockerinit ]] && exit 0 +[[ ! -e /.dockerenv ]] && exit 0 set -xe diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md index eb9bbb67e7d..696c1011eeb 100644 --- a/doc/integration/shibboleth.md +++ b/doc/integration/shibboleth.md @@ -2,7 +2,7 @@ This documentation is for enabling shibboleth with omnibus-gitlab package. -In order to enable Shibboleth support in gitlab we need to use Apache instead of Nginx (It may be possible to use Nginx, however this is difficult to configure using the bundled NIGNX provided in the omnibus-gitlab package). Apache uses mod_shib2 module for shibboleth authentication and can pass attributes as headers to omniauth-shibboleth provider. +In order to enable Shibboleth support in gitlab we need to use Apache instead of Nginx (It may be possible to use Nginx, however this is difficult to configure using the bundled Nginx provided in the omnibus-gitlab package). Apache uses mod_shib2 module for shibboleth authentication and can pass attributes as headers to omniauth-shibboleth provider. To enable the Shibboleth OmniAuth provider you must: diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 85b165ac44f..4d24eb21976 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -267,6 +267,18 @@ GFM also recognizes certain cross-project references: | `namespace/project@9ba12248...b19a04f5` | commit range comparison | | `namespace/project~"Some label"` | issues with given label | +It also has a shorthand version to reference other projects from the same namespace: + +| input | references | +|:------------------------------|:------------------------| +| `project#123` | issue | +| `project!123` | merge request | +| `project%123` | milestone | +| `project$123` | snippet | +| `project@9ba12248` | specific commit | +| `project@9ba12248...b19a04f5` | commit range comparison | +| `project~"Some label"` | issues with given label | + ### Task Lists > If this is not rendered correctly, see diff --git a/doc/user/project/merge_requests/img/preview_issue_for_discussions.png b/doc/user/project/merge_requests/img/preview_issue_for_discussions.png Binary files differnew file mode 100644 index 00000000000..9fdd387676c --- /dev/null +++ b/doc/user/project/merge_requests/img/preview_issue_for_discussions.png diff --git a/doc/user/project/merge_requests/merge_request_discussion_resolution.md b/doc/user/project/merge_requests/merge_request_discussion_resolution.md index 285b1798ac5..f37f1ce4d21 100644 --- a/doc/user/project/merge_requests/merge_request_discussion_resolution.md +++ b/doc/user/project/merge_requests/merge_request_discussion_resolution.md @@ -37,7 +37,8 @@ resolved discussions tracker. > [Introduced][ce-7125] in GitLab 8.14. -You can prevent merge requests from being merged until all discussions are resolved. +You can prevent merge requests from being merged until all discussions are +resolved. Navigate to your project's settings page, select the **Only allow merge requests to be merged if all discussions are resolved** check @@ -50,8 +51,26 @@ are resolved. ![Only allow merge if all the discussions are resolved message](img/only_allow_merge_if_all_discussions_are_resolved_msg.png) +### Move all unresolved discussions in a merge request to an issue + +> [Introduced][ce-7180] (Currently on Backlog) + +To delegate unresolved discussions to a new issue you can click the link **open +an issue to resolve them later**. + +This will prepare an issue with content referring to the merge request and +discussions. + +![Issue mentioning discussions in a merge request](img/preview_issue_for_discussions.png) + +Hitting **Submit issue** will cause all discussions to be marked as resolved and +add a note referring to the newly created issue. + +You can now proceed to merge the merge request from the UI. + [ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022 [ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125 +[ce-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180 [resolve-discussion-button]: img/resolve_discussion_button.png [resolve-comment-button]: img/resolve_comment_button.png [discussion-view]: img/discussion_view.png diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index cab85a48396..b51152c79c6 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -9,7 +9,7 @@ module SharedProject step "project exists in some group namespace" do @group = create(:group, name: 'some group') - @project = create(:project, namespace: @group) + @project = create(:project, namespace: @group, public_builds: false) end # Create a specific project called "Shop" diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 049b4fb214c..26c8f2fecd0 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -151,24 +151,33 @@ module API # Create a new project issue # # Parameters: - # id (required) - The ID of a project - # title (required) - The title of an issue - # description (optional) - The description of an issue - # assignee_id (optional) - The ID of a user to assign issue - # milestone_id (optional) - The ID of a milestone to assign issue - # labels (optional) - The labels of an issue - # created_at (optional) - Date time string, ISO 8601 formatted - # due_date (optional) - Date time string in the format YEAR-MONTH-DAY - # confidential (optional) - Boolean parameter if the issue should be confidential + # id (required) - The ID of a project + # title (required) - The title of an issue + # description (optional) - The description of an issue + # assignee_id (optional) - The ID of a user to assign issue + # milestone_id (optional) - The ID of a milestone to assign issue + # labels (optional) - The labels of an issue + # created_at (optional) - Date time string, ISO 8601 formatted + # due_date (optional) - Date time string in the format YEAR-MONTH-DAY + # confidential (optional) - Boolean parameter if the issue should be confidential + # merge_request_for_resolving_discussions (optional) - The IID of a merge request for which to resolve discussions # Example Request: # POST /projects/:id/issues post ':id/issues' do required_attributes! [:title] - keys = [:title, :description, :assignee_id, :milestone_id, :due_date, :confidential, :labels] + keys = [:title, :description, :assignee_id, :milestone_id, :due_date, :confidential, :labels, :merge_request_for_resolving_discussions] keys << :created_at if current_user.admin? || user_project.owner == current_user attrs = attributes_for_keys(keys) + attrs[:labels] = params[:labels] if params[:labels] + + if merge_request_iid = params[:merge_request_for_resolving_discussions] + attrs[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id). + execute. + find_by(iid: merge_request_iid) + end + # Convert and filter out invalid confidential flags attrs['confidential'] = to_boolean(attrs['confidential']) attrs.delete('confidential') if attrs['confidential'].nil? diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 3740d4fb4cd..d904a8bd4ae 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -33,7 +33,7 @@ module Banzai # Returns a String replaced with the return of the block. def self.references_in(text, pattern = object_class.reference_pattern) text.gsub(pattern) do |match| - yield match, $~[object_sym].to_i, $~[:project], $~ + yield match, $~[object_sym].to_i, $~[:project], $~[:namespace], $~ end end @@ -145,8 +145,9 @@ module Banzai # Returns a String with references replaced with links. All links # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. def object_link_filter(text, pattern, link_content: nil) - references_in(text, pattern) do |match, id, project_ref, matches| - project = project_from_ref_cached(project_ref) + references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches| + project_path = full_project_path(namespace_ref, project_ref) + project = project_from_ref_cached(project_path) if project && object = find_object_cached(project, id) title = object_link_title(object) @@ -217,10 +218,9 @@ module Banzai nodes.each do |node| node.to_html.scan(regex) do - project = $~[:project] || current_project_path + project_path = full_project_path($~[:namespace], $~[:project]) symbol = $~[object_sym] - - refs[project] << symbol if object_class.reference_valid?(symbol) + refs[project_path] << symbol if object_class.reference_valid?(symbol) end end @@ -272,8 +272,19 @@ module Banzai @current_project_path ||= project.path_with_namespace end + def current_project_namespace_path + @current_project_namespace_path ||= project.namespace.path + end + private + def full_project_path(namespace, project_ref) + return current_project_path unless project_ref + + namespace_ref = namespace || current_project_namespace_path + "#{namespace_ref}/#{project_ref}" + end + def project_refs_cache RequestStore[:banzai_project_refs] ||= {} end diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb index 4358bf45549..eaacb9591b1 100644 --- a/lib/banzai/filter/commit_range_reference_filter.rb +++ b/lib/banzai/filter/commit_range_reference_filter.rb @@ -12,7 +12,7 @@ module Banzai def self.references_in(text, pattern = CommitRange.reference_pattern) text.gsub(pattern) do |match| - yield match, $~[:commit_range], $~[:project], $~ + yield match, $~[:commit_range], $~[:project], $~[:namespace], $~ end end diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb index a26dd09c25a..69c06117eda 100644 --- a/lib/banzai/filter/commit_reference_filter.rb +++ b/lib/banzai/filter/commit_reference_filter.rb @@ -12,7 +12,7 @@ module Banzai def self.references_in(text, pattern = Commit.reference_pattern) text.gsub(pattern) do |match| - yield match, $~[:commit], $~[:project], $~ + yield match, $~[:commit], $~[:project], $~[:namespace], $~ end end diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index 9f9a96cdc65..a605dea149e 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -14,16 +14,18 @@ module Banzai def self.references_in(text, pattern = Label.reference_pattern) unescape_html_entities(text).gsub(pattern) do |match| - yield match, $~[:label_id].to_i, $~[:label_name], $~[:project], $~ + yield match, $~[:label_id].to_i, $~[:label_name], $~[:project], $~[:namespace], $~ end end def references_in(text, pattern = Label.reference_pattern) unescape_html_entities(text).gsub(pattern) do |match| - label = find_label($~[:project], $~[:label_id], $~[:label_name]) + namespace, project = $~[:namespace], $~[:project] + project_path = full_project_path(namespace, project) + label = find_label(project_path, $~[:label_id], $~[:label_name]) if label - yield match, label.id, $~[:project], $~ + yield match, label.id, project, namespace, $~ else match end @@ -64,48 +66,12 @@ module Banzai end def object_link_text(object, matches) - if same_group?(object) && namespace_match?(matches) - render_same_project_label(object) - elsif same_project?(object) - render_same_project_label(object) - else - render_cross_project_label(object, matches) - end - end - - def same_group?(object) - object.is_a?(GroupLabel) && object.group == project.group - end - - def namespace_match?(matches) - matches[:project].blank? || matches[:project] == project.path_with_namespace - end - - def same_project?(object) - object.is_a?(ProjectLabel) && object.project == project - end - - def user - context[:current_user] || context[:author] - end - - def project - context[:project] - end - - def render_same_project_label(object) - LabelsHelper.render_colored_label(object) - end - - def render_cross_project_label(object, matches) - source_project = - if matches[:project] - Project.find_with_namespace(matches[:project]) - else - object.project - end + project_path = full_project_path(matches[:namespace], matches[:project]) + project_from_ref = project_from_ref_cached(project_path) + reference = project_from_ref.to_human_reference(project) + label_suffix = " <i>in #{reference}</i>" if reference.present? - LabelsHelper.render_colored_cross_project_label(object, source_project) + LabelsHelper.render_colored_label(object, label_suffix) end def unescape_html_entities(text) diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index 58fff496d00..f12014e191f 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -19,18 +19,20 @@ module Banzai return super(text, pattern) if pattern != Milestone.reference_pattern text.gsub(pattern) do |match| - milestone = find_milestone($~[:project], $~[:milestone_iid], $~[:milestone_name]) + milestone = find_milestone($~[:project], $~[:namespace], $~[:milestone_iid], $~[:milestone_name]) if milestone - yield match, milestone.iid, $~[:project], $~ + yield match, milestone.iid, $~[:project], $~[:namespace], $~ else match end end end - def find_milestone(project_ref, milestone_id, milestone_name) - project = project_from_ref(project_ref) + def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name) + project_path = full_project_path(namespace_ref, project_ref) + project = project_from_ref(project_path) + return unless project milestone_params = milestone_params(milestone_id, milestone_name) @@ -52,11 +54,13 @@ module Banzai end def object_link_text(object, matches) - if context[:project] == object.project - super + milestone_link = escape_once(super) + reference = object.project.to_reference(project) + + if reference.present? + "#{milestone_link} <i>in #{reference}</i>".html_safe else - "#{escape_once(super)} <i>in #{escape_once(object.project.path_with_namespace)}</i>". - html_safe + milestone_link end end diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb new file mode 100644 index 00000000000..dd6d99e9075 --- /dev/null +++ b/lib/gitlab/ci/status/canceled.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Canceled < Status::Core + def text + 'canceled' + end + + def label + 'canceled' + end + + def icon + 'icon_status_canceled' + end + end + end + end +end diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb new file mode 100644 index 00000000000..ce4108fdcf2 --- /dev/null +++ b/lib/gitlab/ci/status/core.rb @@ -0,0 +1,58 @@ +module Gitlab + module Ci + module Status + # Base abstract class fore core status + # + class Core + include Gitlab::Routing.url_helpers + + def initialize(subject) + @subject = subject + end + + def icon + raise NotImplementedError + end + + def label + raise NotImplementedError + end + + def title + "#{@subject.class.name.demodulize}: #{label}" + end + + # Deprecation warning: this method is here because we need to maintain + # backwards compatibility with legacy statuses. We often do something + # like "ci-status ci-status-#{status}" to set CSS class. + # + # `to_s` method should be renamed to `group` at some point, after + # phasing legacy satuses out. + # + def to_s + self.class.name.demodulize.downcase.underscore + end + + def has_details? + raise NotImplementedError + end + + def details_path + raise NotImplementedError + end + + def has_action? + raise NotImplementedError + end + + def action_icon + raise NotImplementedError + end + + def action_path + raise NotImplementedError + end + end + end + end +end diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb new file mode 100644 index 00000000000..6596d7e01ca --- /dev/null +++ b/lib/gitlab/ci/status/created.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Created < Status::Core + def text + 'created' + end + + def label + 'created' + end + + def icon + 'icon_status_created' + end + end + end + end +end diff --git a/lib/gitlab/ci/status/extended.rb b/lib/gitlab/ci/status/extended.rb new file mode 100644 index 00000000000..6bfb5d38c1f --- /dev/null +++ b/lib/gitlab/ci/status/extended.rb @@ -0,0 +1,11 @@ +module Gitlab + module Ci + module Status + module Extended + def matches?(_subject) + raise NotImplementedError + end + end + end + end +end diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb new file mode 100644 index 00000000000..c5b5e3203ad --- /dev/null +++ b/lib/gitlab/ci/status/failed.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Failed < Status::Core + def text + 'failed' + end + + def label + 'failed' + end + + def icon + 'icon_status_failed' + end + end + end + end +end diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb new file mode 100644 index 00000000000..d30f35a59a2 --- /dev/null +++ b/lib/gitlab/ci/status/pending.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Pending < Status::Core + def text + 'pending' + end + + def label + 'pending' + end + + def icon + 'icon_status_pending' + end + end + end + end +end diff --git a/lib/gitlab/ci/status/pipeline/common.rb b/lib/gitlab/ci/status/pipeline/common.rb new file mode 100644 index 00000000000..25e52bec3da --- /dev/null +++ b/lib/gitlab/ci/status/pipeline/common.rb @@ -0,0 +1,23 @@ +module Gitlab + module Ci + module Status + module Pipeline + module Common + def has_details? + true + end + + def details_path + namespace_project_pipeline_path(@subject.project.namespace, + @subject.project, + @subject) + end + + def has_action? + false + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb new file mode 100644 index 00000000000..71d27bf7cf5 --- /dev/null +++ b/lib/gitlab/ci/status/pipeline/factory.rb @@ -0,0 +1,39 @@ +module Gitlab + module Ci + module Status + module Pipeline + class Factory + EXTENDED_STATUSES = [Pipeline::SuccessWithWarnings] + + def initialize(pipeline) + @pipeline = pipeline + @status = pipeline.status || :created + end + + def fabricate! + if extended_status + extended_status.new(core_status) + else + core_status + end + end + + private + + def core_status + Gitlab::Ci::Status + .const_get(@status.capitalize) + .new(@pipeline) + .extend(Status::Pipeline::Common) + end + + def extended_status + @extended ||= EXTENDED_STATUSES.find do |status| + status.matches?(@pipeline) + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb new file mode 100644 index 00000000000..4b040d60df8 --- /dev/null +++ b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb @@ -0,0 +1,31 @@ +module Gitlab + module Ci + module Status + module Pipeline + class SuccessWithWarnings < SimpleDelegator + extend Status::Extended + + def text + 'passed' + end + + def label + 'passed with warnings' + end + + def icon + 'icon_status_warning' + end + + def to_s + 'success_with_warnings' + end + + def self.matches?(pipeline) + pipeline.success? && pipeline.has_warnings? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb new file mode 100644 index 00000000000..2aba3c373c7 --- /dev/null +++ b/lib/gitlab/ci/status/running.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Running < Status::Core + def text + 'running' + end + + def label + 'running' + end + + def icon + 'icon_status_running' + end + end + end + end +end diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb new file mode 100644 index 00000000000..16282aefd03 --- /dev/null +++ b/lib/gitlab/ci/status/skipped.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Skipped < Status::Core + def text + 'skipped' + end + + def label + 'skipped' + end + + def icon + 'icon_status_skipped' + end + end + end + end +end diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb new file mode 100644 index 00000000000..c09c5f006e3 --- /dev/null +++ b/lib/gitlab/ci/status/success.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Success < Status::Core + def text + 'passed' + end + + def label + 'passed' + end + + def icon + 'icon_status_success' + end + end + end + end +end diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh index 7c4e8276902..62236ed539a 100755 --- a/scripts/lint-doc.sh +++ b/scripts/lint-doc.sh @@ -3,7 +3,7 @@ cd "$(dirname "$0")/.." # Use long options (e.g. --header instead of -H) for curl examples in documentation. -grep --perl-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/ +grep --extended-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/ if [ $? == 0 ] then echo '✖ ERROR: Short options should not be used in documentation!' >&2 diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 90419368f22..dbe5ddccbcf 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -55,6 +55,30 @@ describe Projects::IssuesController do end describe 'GET #new' do + context 'internal issue tracker' do + before do + sign_in(user) + project.team << [user, :developer] + end + + it 'builds a new issue' do + get :new, namespace_id: project.namespace.path, project_id: project + + expect(assigns(:issue)).to be_a_new(Issue) + end + + it 'fills in an issue for a merge request' do + project_with_repository = create(:project) + project_with_repository.team << [user, :developer] + mr = create(:merge_request_with_diff_notes, source_project: project_with_repository) + + get :new, namespace_id: project_with_repository.namespace.path, project_id: project_with_repository, merge_request_for_resolving_discussions: mr.iid + + expect(assigns(:issue).title).not_to be_empty + expect(assigns(:issue).description).not_to be_empty + end + end + context 'external issue tracker' do it 'redirects to the external issue tracker' do external = double(new_issue_path: 'https://example.com/issues/new') @@ -272,6 +296,42 @@ describe Projects::IssuesController do end describe 'POST #create' do + context 'resolving discussions in MergeRequest' do + let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first } + let(:merge_request) { discussion.noteable } + let(:project) { merge_request.source_project } + + before do + project.team << [user, :master] + sign_in user + end + + let(:merge_request_params) do + { merge_request_for_resolving_discussions: merge_request.iid } + end + + def post_issue(issue_params) + post :create, namespace_id: project.namespace.to_param, project_id: project.to_param, issue: issue_params, merge_request_for_resolving_discussions: merge_request.iid + end + + it 'creates an issue for the project' do + expect { post_issue({ title: 'Hello' }) }.to change { project.issues.reload.size }.by(1) + end + + it "doesn't overwrite given params" do + post_issue(description: 'Manually entered description') + + expect(assigns(:issue).description).to eq('Manually entered description') + end + + it 'resolves the discussion in the merge_request' do + post_issue(title: 'Hello') + discussion.first_note.reload + + expect(discussion.resolved?).to eq(true) + end + end + context 'Akismet is enabled' do before do allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) diff --git a/spec/features/groups/labels/edit_spec.rb b/spec/features/groups/labels/edit_spec.rb new file mode 100644 index 00000000000..69281cecb7b --- /dev/null +++ b/spec/features/groups/labels/edit_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +feature 'Edit group label', feature: true do + given(:user) { create(:user) } + given(:group) { create(:group) } + given(:label) { create(:group_label, group: group) } + + background do + group.add_owner(user) + login_as(user) + visit edit_group_label_path(group, label) + end + + scenario 'update label with new title' do + fill_in 'label_title', with: 'new label name' + click_button 'Save changes' + + expect(current_path).to eq(root_path) + expect(label.reload.title).to eq('new label name') + end +end diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb index a2791b57544..30b80aa82b0 100644 --- a/spec/features/groups/merge_requests_spec.rb +++ b/spec/features/groups/merge_requests_spec.rb @@ -2,7 +2,35 @@ 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")} + 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 + + context 'archived issuable' do + let(:project_archived) { create(:project, group: group, merge_requests_access_level: ProjectFeature::ENABLED, archived: true) } + let(:issuable_archived) { create(:merge_request, source_project: project_archived, target_project: project_archived, title: 'issuable of an archived project') } + let(:access_level) { ProjectFeature::ENABLED } + let(:user) { user_in_group } + + before do + issuable_archived + visit path + end + + it 'hides archived merge requests' do + expect(page).to have_content(issuable.title) + expect(page).not_to have_content(issuable_archived.title) + end + + it 'ignores archived merge request count badges in navbar' do + expect( page.find('[title="Merge Requests"] span.badge.count').text).to eq("1") + end + + it 'ignores archived merge request count badges in state-filters' do + expect(page.find('#state-opened span.badge').text).to eq("1") + expect(page.find('#state-merged span.badge').text).to eq("0") + expect(page.find('#state-closed span.badge').text).to eq("0") + expect(page.find('#state-all span.badge').text).to eq("1") + end + end end diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb new file mode 100644 index 00000000000..762cab0c0e1 --- /dev/null +++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb @@ -0,0 +1,76 @@ +require 'rails_helper' + +feature 'Resolving all open discussions in a merge request from an issue', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: true) } + let(:merge_request) { create(:merge_request, source_project: project) } + let!(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request, noteable: merge_request, project: project)]).first } + + before do + project.team << [user, :master] + login_as user + end + + context 'with the internal tracker disabled' do + before do + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'does not show a link to create a new issue' do + expect(page).not_to have_link 'open an issue to resolve them later' + end + end + + context 'merge request has discussions that need to be resolved' do + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'shows a warning that the merge request contains unresolved discussions' do + expect(page).to have_content 'This merge request has unresolved discussions' + end + + it 'has a link to resolve all discussions by creating an issue' do + page.within '.mr-widget-body' do + expect(page).to have_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_for_resolving_discussions: merge_request.iid) + end + end + + context 'creating an issue for discussions' do + before do + page.click_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_for_resolving_discussions: merge_request.iid) + end + + it 'shows an issue with the title filled in' do + title_field = page.find_field('issue[title]') + + expect(title_field.value).to include(merge_request.title) + end + + it 'has a mention of the discussion in the description' do + description_field = page.find_field('issue[description]') + + expect(description_field.value).to include(discussion.first_note.note) + end + + it 'has a hidden field for the merge request' do + merge_request_field = find('#merge_request_for_resolving_discussions', visible: false) + + expect(merge_request_field.value).to eq(merge_request.iid.to_s) + end + + it 'can create a new issue for the project' do + expect { click_button 'Submit issue' }.to change { project.issues.reload.size }.by(1) + end + + it 'resolves the discussion in the merge request' do + click_button 'Submit issue' + + discussion.first_note.reload + + expect(discussion.resolved?).to eq(true) + end + end + end +end diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index c9bec05a9da..f89b4db9e62 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -28,7 +28,7 @@ feature 'issue move to another project' do let(:new_project) { create(:project) } let(:new_project_search) { create(:project) } let(:text) { "Text with #{mr.to_reference}" } - let(:cross_reference) { old_project.to_reference } + let(:cross_reference) { old_project.to_reference(new_project) } background do old_project.team << [user, :reporter] diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index 702869b6e8b..f1b68a39343 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -40,7 +40,7 @@ feature 'Create New Merge Request', feature: true, js: true do visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_project_id: private_project.id }) - expect(page).not_to have_content private_project.to_reference + expect(page).not_to have_content private_project.path_with_namespace end end diff --git a/spec/features/merge_requests/deleted_source_branch_spec.rb b/spec/features/merge_requests/deleted_source_branch_spec.rb index 778b3a90cf3..d5c9ed8a3b7 100644 --- a/spec/features/merge_requests/deleted_source_branch_spec.rb +++ b/spec/features/merge_requests/deleted_source_branch_spec.rb @@ -1,5 +1,8 @@ require 'spec_helper' +# This test serves as a regression test for a bug that caused an error +# message to be shown by JavaScript when the source branch was deleted. +# Please do not remove "js: true". describe 'Deleted source branch', feature: true, js: true do let(:user) { create(:user) } let(:merge_request) { create(:merge_request) } diff --git a/spec/features/projects/guest_navigation_menu_spec.rb b/spec/features/projects/guest_navigation_menu_spec.rb index c22441f8929..8120a51c515 100644 --- a/spec/features/projects/guest_navigation_menu_spec.rb +++ b/spec/features/projects/guest_navigation_menu_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' describe "Guest navigation menu" do - let(:project) { create :empty_project, :private } - let(:guest) { create :user } + let(:project) { create(:empty_project, :private, public_builds: false) } + let(:guest) { create(:user) } before do project.team << [guest, :guest] diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index 290ddb4c6dd..f52e23f9433 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe "Private Project Access", feature: true do include AccessMatchers - let(:project) { create(:project, :private) } + let(:project) { create(:project, :private, public_builds: false) } describe "Project should be private" do describe '#private?' do @@ -260,6 +260,18 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:external) } it { is_expected.to be_denied_for(:visitor) } + + context 'when public builds is enabled' do + before do + project.update(public_builds: true) + end + + it { is_expected.to be_allowed_for(:guest).of(project) } + end + + context 'when public buils are disabled' do + it { is_expected.to be_denied_for(:guest).of(project) } + end end describe "GET /:project_path/pipelines/:id" do @@ -275,6 +287,18 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:external) } it { is_expected.to be_denied_for(:visitor) } + + context 'when public builds is enabled' do + before do + project.update(public_builds: true) + end + + it { is_expected.to be_allowed_for(:guest).of(project) } + end + + context 'when public buils are disabled' do + it { is_expected.to be_denied_for(:guest).of(project) } + end end describe "GET /:project_path/builds" do @@ -289,6 +313,18 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:external) } it { is_expected.to be_denied_for(:visitor) } + + context 'when public builds is enabled' do + before do + project.update(public_builds: true) + end + + it { is_expected.to be_allowed_for(:guest).of(project) } + end + + context 'when public buils are disabled' do + it { is_expected.to be_denied_for(:guest).of(project) } + end end describe "GET /:project_path/builds/:id" do @@ -305,6 +341,23 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:external) } it { is_expected.to be_denied_for(:visitor) } + + context 'when public builds is enabled' do + before do + project.update(public_builds: true) + end + + it { is_expected.to be_allowed_for(:guest).of(project) } + end + + context 'when public buils are disabled' do + before do + project.public_builds = false + project.save + end + + it { is_expected.to be_denied_for(:guest).of(project) } + end end describe "GET /:project_path/environments" do diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 40bccb8e50b..7f69e888f32 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -10,6 +10,7 @@ describe IssuesFinder do let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') } let(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') } let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) } + let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') } let!(:label_link) { create(:label_link, label: label, target: issue2) } before do @@ -25,7 +26,7 @@ describe IssuesFinder do describe '#execute' do let(:search_user) { user } let(:params) { {} } - let(:issues) { IssuesFinder.new(search_user, params.merge(scope: scope, state: 'opened')).execute } + let(:issues) { IssuesFinder.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute } context 'scope: all' do let(:scope) { 'all' } @@ -143,6 +144,40 @@ describe IssuesFinder do end end + context 'filtering by state' do + context 'with opened' do + let(:params) { { state: 'opened' } } + + it 'returns only opened issues' do + expect(issues).to contain_exactly(issue1, issue2, issue3) + end + end + + context 'with closed' do + let(:params) { { state: 'closed' } } + + it 'returns only closed issues' do + expect(issues).to contain_exactly(closed_issue) + end + end + + context 'with all' do + let(:params) { { state: 'all' } } + + it 'returns all issues' do + expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue) + end + end + + context 'with invalid state' do + let(:params) { { state: 'invalid_state' } } + + it 'returns all issues' do + expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue) + end + end + end + context 'when the user is unauthorized' do let(:search_user) { nil } diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 535aabfc18d..88361e27102 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -6,14 +6,17 @@ describe MergeRequestsFinder do let(:project1) { create(:project) } let(:project2) { create(:project, forked_from_project: project1) } + let(:project3) { create(:project, forked_from_project: project1, archived: true) } let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) } let!(:merge_request2) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1, state: 'closed') } let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2) } + let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3) } before do project1.team << [user, :master] project2.team << [user, :developer] + project3.team << [user, :developer] project2.team << [user2, :developer] end @@ -21,7 +24,7 @@ describe MergeRequestsFinder do it 'filters by scope' do params = { scope: 'authored', state: 'opened' } merge_requests = MergeRequestsFinder.new(user, params).execute - expect(merge_requests.size).to eq(2) + expect(merge_requests.size).to eq(3) end it 'filters by project' do @@ -29,5 +32,11 @@ describe MergeRequestsFinder do merge_requests = MergeRequestsFinder.new(user, params).execute expect(merge_requests.size).to eq(1) end + + it 'filters by non_archived' do + params = { non_archived: true } + merge_requests = MergeRequestsFinder.new(user, params).execute + expect(merge_requests.size).to eq(3) + end end end diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index d30daf47543..7cf535fadae 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -7,7 +7,7 @@ describe LabelsHelper do context 'without subject' do it "uses the label's project" do - expect(link_to_label(label)).to match %r{<a href="/#{label.project.to_reference}/issues\?label_name%5B%5D=#{label.name}">.*</a>} + expect(link_to_label(label)).to match %r{<a href="/#{label.project.path_with_namespace}/issues\?label_name%5B%5D=#{label.name}">.*</a>} end end @@ -32,7 +32,7 @@ describe LabelsHelper do ['issue', :issue, 'merge_request', :merge_request].each do |type| context "set to #{type}" do it 'links to correct page' do - expect(link_to_label(label, type: type)).to match %r{<a href="/#{label.project.to_reference}/#{type.to_s.pluralize}\?label_name%5B%5D=#{label.name}">.*</a>} + expect(link_to_label(label, type: type)).to match %r{<a href="/#{label.project.path_with_namespace}/#{type.to_s.pluralize}\?label_name%5B%5D=#{label.name}">.*</a>} end end end diff --git a/spec/lib/banzai/filter/abstract_link_filter_spec.rb b/spec/lib/banzai/filter/abstract_link_filter_spec.rb index 1ee31a603e4..70a87fbc01e 100644 --- a/spec/lib/banzai/filter/abstract_link_filter_spec.rb +++ b/spec/lib/banzai/filter/abstract_link_filter_spec.rb @@ -5,7 +5,7 @@ describe Banzai::Filter::AbstractReferenceFilter do describe '#references_per_project' do it 'returns a Hash containing references grouped per project paths' do - doc = Nokogiri::HTML.fragment("#1 #{project.to_reference}#2") + doc = Nokogiri::HTML.fragment("#1 #{project.path_with_namespace}#2") filter = described_class.new(doc, project: project) expect(filter).to receive(:object_class).exactly(4).times.and_return(Issue) @@ -14,7 +14,7 @@ describe Banzai::Filter::AbstractReferenceFilter do refs = filter.references_per_project expect(refs).to be_an_instance_of(Hash) - expect(refs[project.to_reference]).to eq(Set.new(%w[1 2])) + expect(refs[project.path_with_namespace]).to eq(Set.new(%w[1 2])) end end diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb index e6c90ad87ee..9703e2315b8 100644 --- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb @@ -59,9 +59,7 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do it 'ignores invalid commit IDs' do exp = act = "See #{commit1.id.reverse}...#{commit2.id}" - expect(project).to receive(:valid_repo?).and_return(true) - expect(project.repository).to receive(:commit).with(commit1.id.reverse) - expect(project.repository).to receive(:commit).with(commit2.id) + allow(project.repository).to receive(:commit).with(commit1.id.reverse) expect(reference_filter(act).to_html).to eq exp end @@ -100,14 +98,44 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do end end - context 'cross-project reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, :public, namespace: namespace) } - let(:reference) { range.to_reference(project) } + context 'cross-project / cross-namespace complete reference' do + let(:project2) { create(:project, :public) } + let(:reference) { "#{project2.path_with_namespace}@#{commit1.id}...#{commit2.id}" } - before do - range.project = project2 + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) + end + + it 'link has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.css('a').first.text). + to eql("#{project2.path_with_namespace}@#{commit1.short_id}...#{commit2.short_id}") + end + + it 'has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.text).to eql("Fixed (#{project2.path_with_namespace}@#{commit1.short_id}...#{commit2.short_id}.)") + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Fixed #{project2.path_with_namespace}@#{commit1.id.reverse}...#{commit2.id}" + expect(reference_filter(act).to_html).to eq exp + + exp = act = "Fixed #{project2.path_with_namespace}@#{commit1.id}...#{commit2.id.reverse}" + expect(reference_filter(act).to_html).to eq exp end + end + + context 'cross-project / same-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:project, :public, namespace: namespace) } + let(:project2) { create(:project, :public, path: "same-namespace", namespace: namespace) } + let(:reference) { "#{project2.path}@#{commit1.id}...#{commit2.id}" } it 'links to a valid reference' do doc = reference_filter("See #{reference}") @@ -116,24 +144,65 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) end - it 'links with adjacent text' do + it 'link has valid text' do doc = reference_filter("Fixed (#{reference}.)") - exp = Regexp.escape("#{project2.to_reference}@#{range.reference_link_text}") - expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) + expect(doc.css('a').first.text). + to eql("#{project2.path}@#{commit1.short_id}...#{commit2.short_id}") + end + + it 'has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.text).to eql("Fixed (#{project2.path}@#{commit1.short_id}...#{commit2.short_id}.)") end it 'ignores invalid commit IDs on the referenced project' do - exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" + exp = act = "Fixed #{project2.path}@#{commit1.id.reverse}...#{commit2.id}" expect(reference_filter(act).to_html).to eq exp - exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" + exp = act = "Fixed #{project2.path}@#{commit1.id}...#{commit2.id.reverse}" + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project shorthand reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:project, :public, namespace: namespace) } + let(:project2) { create(:project, :public, path: "same-namespace", namespace: namespace) } + let(:reference) { "#{project2.path}@#{commit1.id}...#{commit2.id}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) + end + + it 'link has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.css('a').first.text). + to eql("#{project2.path}@#{commit1.short_id}...#{commit2.short_id}") + end + + it 'has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.text).to eql("Fixed (#{project2.path}@#{commit1.short_id}...#{commit2.short_id}.)") + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Fixed #{project2.path}@#{commit1.id.reverse}...#{commit2.id}" + expect(reference_filter(act).to_html).to eq exp + + exp = act = "Fixed #{project2.path}@#{commit1.id}...#{commit2.id.reverse}" expect(reference_filter(act).to_html).to eq exp end end context 'cross-project URL reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:namespace) { create(:namespace) } let(:project2) { create(:project, :public, namespace: namespace) } let(:range) { CommitRange.new("#{commit1.id}...master", project) } let(:reference) { urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: 'master') } diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb index e0f08282551..2e6dcc3a434 100644 --- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb @@ -41,6 +41,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do it 'links with adjacent text' do doc = reference_filter("See (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{commit.short_id}<\/a>\.\)/) end @@ -48,8 +49,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do invalid = invalidate_reference(reference) exp = act = "See #{invalid}" - expect(project).to receive(:valid_repo?).and_return(true) - expect(project.repository).to receive(:commit).with(invalid) expect(reference_filter(act).to_html).to eq exp end @@ -95,34 +94,85 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do end end - context 'cross-project reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } + context 'cross-project / cross-namespace complete reference' do + let(:namespace) { create(:namespace) } let(:project2) { create(:project, :public, namespace: namespace) } let(:commit) { project2.commit } - let(:reference) { commit.to_reference(project) } + let(:reference) { "#{project2.path_with_namespace}@#{commit.short_id}" } - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") + it 'link has valid text' do + doc = reference_filter("See (#{reference}.)") - expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) + expect(doc.css('a').first.text).to eql("#{project2.path_with_namespace}@#{commit.short_id}") end - it 'links with adjacent text' do - doc = reference_filter("Fixed (#{reference}.)") + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text).to eql("See (#{project2.path_with_namespace}@#{commit.short_id}.)") + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Committed #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project / same-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, namespace: namespace) } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:commit) { project2.commit } + let(:reference) { "#{project2.path_with_namespace}@#{commit.short_id}" } + + it 'link has valid text' do + doc = reference_filter("See (#{reference}.)") - exp = Regexp.escape(project2.to_reference) - expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/) + expect(doc.css('a').first.text).to eql("#{project2.path}@#{commit.short_id}") + end + + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text).to eql("See (#{project2.path}@#{commit.short_id}.)") end it 'ignores invalid commit IDs on the referenced project' do exp = act = "Committed #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project shorthand reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, namespace: namespace) } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:commit) { project2.commit } + let(:reference) { "#{project2.path_with_namespace}@#{commit.short_id}" } + + it 'link has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.css('a').first.text).to eql("#{project2.path}@#{commit.short_id}") + end + + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text).to eql("See (#{project2.path}@#{commit.short_id}.)") + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Committed #{invalidate_reference(reference)}" + expect(reference_filter(act).to_html).to eq exp end end context 'cross-project URL reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:namespace) { create(:namespace) } let(:project2) { create(:project, :public, namespace: namespace) } let(:commit) { project2.commit } let(:reference) { urls.namespace_project_commit_url(project2.namespace, project2, commit.id) } diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 8f0b2db3e8e..456dbac0698 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -8,7 +8,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end let(:project) { create(:empty_project, :public) } - let(:issue) { create(:issue, project: project) } + let(:issue) { create(:issue, project: project) } it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) @@ -24,7 +24,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do context 'internal reference' do it_behaves_like 'a reference containing an element node' - let(:reference) { issue.to_reference } + let(:reference) { "##{issue.iid}" } it 'ignores valid references when using non-default tracker' do allow(project).to receive(:default_issues_tracker?).and_return(false) @@ -42,7 +42,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do it 'links with adjacent text' do doc = reference_filter("Fixed (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + expect(doc.text).to eql("Fixed (#{reference}.)") end it 'ignores invalid issue IDs' do @@ -116,13 +116,56 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end end - context 'cross-project reference' do + context 'cross-project / cross-namespace complete reference' do it_behaves_like 'a reference containing an element node' - let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { "#{project2.path_with_namespace}##{issue.iid}" } + + it 'ignores valid references when cross-reference project uses external tracker' do + expect_any_instance_of(described_class).to receive(:find_object). + with(project2, issue.iid). + and_return(nil) + + exp = act = "Issue #{reference}" + expect(reference_filter(act).to_html).to eq exp + end + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project2) + end + + it 'link has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.css('a').first.text).to eql("#{project2.path_with_namespace}##{issue.iid}") + end + + it 'has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.text).to eq("Fixed (#{project2.path_with_namespace}##{issue.iid}.)") + end + + it 'ignores invalid issue IDs on the referenced project' do + exp = act = "Fixed #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project / same-namespace complete reference' do + it_behaves_like 'a reference containing an element node' + + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } let(:project2) { create(:empty_project, :public, namespace: namespace) } let(:issue) { create(:issue, project: project2) } - let(:reference) { issue.to_reference(project) } + let(:reference) { "#{project2.path_with_namespace}##{issue.iid}" } it 'ignores valid references when cross-reference project uses external tracker' do expect_any_instance_of(described_class).to receive(:find_object). @@ -140,9 +183,16 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do to eq helper.url_for_issue(issue.iid, project2) end - it 'links with adjacent text' do + it 'link has valid text' do doc = reference_filter("Fixed (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + + expect(doc.css('a').first.text).to eql("#{project2.path}##{issue.iid}") + end + + it 'has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.text).to eq("Fixed (#{project2.path}##{issue.iid}.)") end it 'ignores invalid issue IDs on the referenced project' do @@ -150,9 +200,47 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do expect(reference_filter(act).to_html).to eq exp end + end + + context 'cross-project shorthand reference' do + it_behaves_like 'a reference containing an element node' - it 'ignores out-of-bounds issue IDs on the referenced project' do - exp = act = "Fixed ##{Gitlab::Database::MAX_INT_VALUE + 1}" + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { "#{project2.path}##{issue.iid}" } + + it 'ignores valid references when cross-reference project uses external tracker' do + expect_any_instance_of(described_class).to receive(:find_object). + with(project2, issue.iid). + and_return(nil) + + exp = act = "Issue #{reference}" + expect(reference_filter(act).to_html).to eq exp + end + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project2) + end + + it 'link has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.css('a').first.text).to eql("#{project2.path}##{issue.iid}") + end + + it 'has valid text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.text).to eq("Fixed (#{project2.path}##{issue.iid}.)") + end + + it 'ignores invalid issue IDs on the referenced project' do + exp = act = "Fixed #{invalidate_reference(reference)}" expect(reference_filter(act).to_html).to eq exp end diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 9c09f00ae8a..284641fb20a 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -4,7 +4,7 @@ require 'html/pipeline' describe Banzai::Filter::LabelReferenceFilter, lib: true do include FilterSpecHelper - let(:project) { create(:empty_project, :public) } + let(:project) { create(:empty_project, :public, name: 'sample-project') } let(:label) { create(:label, project: project) } let(:reference) { label.to_reference } @@ -48,6 +48,14 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name) end + context 'project that does not exist referenced' do + let(:result) { reference_filter('aaa/bbb~ccc') } + + it 'does not link reference' do + expect(result.to_html).to eq 'aaa/bbb~ccc' + end + end + describe 'label span element' do it 'includes default classes' do doc = reference_filter("Label #{reference}") @@ -334,14 +342,14 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do end context 'with project reference' do - let(:reference) { project.to_reference + group_label.to_reference(format: :name) } + let(:reference) { "#{project.to_reference}#{group_label.to_reference(format: :name)}" } it 'links to a valid reference' do doc = reference_filter("See #{reference}", project: project) expect(doc.css('a').first.attr('href')).to eq urls. namespace_project_issues_url(project.namespace, project, label_name: group_label.name) - expect(doc.text).to eq 'See gfm references' + expect(doc.text).to eq "See gfm references" end it 'links with adjacent text' do @@ -357,68 +365,247 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do end end - describe 'cross project label references' do - context 'valid project referenced' do - let(:another_project) { create(:empty_project, :public) } - let(:project_name) { another_project.name_with_namespace } - let(:label) { create(:label, project: another_project, color: '#00ff00') } - let(:reference) { label.to_reference(project) } + describe 'cross-project / cross-namespace complete reference' do + let(:project2) { create(:empty_project) } + let(:label) { create(:label, project: project2, color: '#00ff00') } + let(:reference) { "#{project2.path_with_namespace}~#{label.name}" } + let!(:result) { reference_filter("See #{reference}") } - let!(:result) { reference_filter("See #{reference}") } + it 'links to a valid reference' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(project2.namespace, + project2, + label_name: label.name) + end - it 'points to referenced project issues page' do - expect(result.css('a').first.attr('href')) - .to eq urls.namespace_project_issues_url(another_project.namespace, - another_project, - label_name: label.name) - end + it 'has valid color' do + expect(result.css('a span').first.attr('style')).to match /background-color: #00ff00/ + end - it 'has valid color' do - expect(result.css('a span').first.attr('style')) - .to match /background-color: #00ff00/ - end + it 'has valid link text' do + expect(result.css('a').first.text).to eq "#{label.name} in #{project2.name_with_namespace}" + end - it 'contains cross project content' do - expect(result.css('a').first.text).to eq "#{label.name} in #{project_name}" - end + it 'has valid text' do + expect(result.text).to eq "See #{label.name} in #{project2.name_with_namespace}" end - context 'project that does not exist referenced' do - let(:result) { reference_filter('aaa/bbb~ccc') } + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" - it 'does not link reference' do - expect(result.to_html).to eq 'aaa/bbb~ccc' - end + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'cross-project / same-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, namespace: namespace) } + let(:project2) { create(:empty_project, namespace: namespace) } + let(:label) { create(:label, project: project2, color: '#00ff00') } + let(:reference) { "#{project2.path_with_namespace}~#{label.name}" } + let!(:result) { reference_filter("See #{reference}") } + + it 'links to a valid reference' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(project2.namespace, + project2, + label_name: label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')).to match /background-color: #00ff00/ + end + + it 'has valid link text' do + expect(result.css('a').first.text).to eq "#{label.name} in #{project2.name}" + end + + it 'has valid text' do + expect(result.text).to eq "See #{label.name} in #{project2.name}" + end + + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'cross-project shorthand reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, namespace: namespace) } + let(:project2) { create(:empty_project, namespace: namespace) } + let(:label) { create(:label, project: project2, color: '#00ff00') } + let(:reference) { "#{project2.path}~#{label.name}" } + let!(:result) { reference_filter("See #{reference}") } + + it 'links to a valid reference' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(project2.namespace, + project2, + label_name: label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')). + to match /background-color: #00ff00/ + end + + it 'has valid link text' do + expect(result.css('a').first.text).to eq "#{label.name} in #{project2.name}" + end + + it 'has valid text' do + expect(result.text).to eq "See #{label.name} in #{project2.name}" + end + + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp end end describe 'cross group label references' do - context 'valid project referenced' do - let(:group) { create(:group) } - let(:project) { create(:empty_project, :public, namespace: group) } - let(:another_group) { create(:group) } - let(:another_project) { create(:empty_project, :public, namespace: another_group) } - let(:project_name) { another_project.name_with_namespace } - let(:group_label) { create(:group_label, group: another_group, color: '#00ff00') } - let(:reference) { another_project.to_reference + group_label.to_reference } - - let!(:result) { reference_filter("See #{reference}", project: project) } - - it 'points to referenced project issues page' do - expect(result.css('a').first.attr('href')) - .to eq urls.namespace_project_issues_url(another_project.namespace, - another_project, - label_name: group_label.name) - end + let(:group) { create(:group) } + let(:project) { create(:empty_project, :public, namespace: group) } + let(:another_group) { create(:group) } + let(:another_project) { create(:empty_project, :public, namespace: another_group) } + let(:group_label) { create(:group_label, group: another_group, color: '#00ff00') } + let(:reference) { "#{another_project.path_with_namespace}~#{group_label.name}" } + let!(:result) { reference_filter("See #{reference}", project: project) } - it 'has valid color' do - expect(result.css('a span').first.attr('style')) - .to match /background-color: #00ff00/ - end + it 'points to referenced project issues page' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(another_project.namespace, + another_project, + label_name: group_label.name) + end - it 'contains cross project content' do - expect(result.css('a').first.text).to eq "#{group_label.name} in #{project_name}" - end + it 'has valid color' do + expect(result.css('a span').first.attr('style')). + to match /background-color: #00ff00/ + end + + it 'has valid link text' do + expect(result.css('a').first.text). + to eq "#{group_label.name} in #{another_project.name_with_namespace}" + end + + it 'has valid text' do + expect(result.text). + to eq "See #{group_label.name} in #{another_project.name_with_namespace}" + end + + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'cross-project / same-group_label complete reference' do + let(:group) { create(:group) } + let(:project) { create(:empty_project, :public, namespace: group) } + let(:another_project) { create(:empty_project, :public, namespace: group) } + let(:group_label) { create(:group_label, group: group, color: '#00ff00') } + let(:reference) { "#{another_project.path_with_namespace}~#{group_label.name}" } + let!(:result) { reference_filter("See #{reference}", project: project) } + + it 'points to referenced project issues page' do + expect(result.css('a').first.attr('href')). + to eq urls.namespace_project_issues_url(another_project.namespace, + another_project, + label_name: group_label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')). + to match /background-color: #00ff00/ + end + + it 'has valid link text' do + expect(result.css('a').first.text). + to eq "#{group_label.name} in #{another_project.name}" + end + + it 'has valid text' do + expect(result.text). + to eq "See #{group_label.name} in #{another_project.name}" + end + + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'same project / same group_label complete reference' do + let(:group) { create(:group) } + let(:project) { create(:empty_project, :public, namespace: group) } + let(:group_label) { create(:group_label, group: group, color: '#00ff00') } + let(:reference) { "#{project.path_with_namespace}~#{group_label.name}" } + let!(:result) { reference_filter("See #{reference}", project: project) } + + it 'points to referenced project issues page' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(project.namespace, + project, + label_name: group_label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')) + .to match /background-color: #00ff00/ + end + + it 'has valid link text' do + expect(result.css('a').first.text).to eq group_label.name + end + + it 'has valid text' do + expect(result.text).to eq "See #{group_label.name}" + end + + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'same project / same group_label shorthand reference' do + let(:group) { create(:group) } + let(:project) { create(:empty_project, :public, namespace: group) } + let(:group_label) { create(:group_label, group: group, color: '#00ff00') } + let(:reference) { "#{project.path}~#{group_label.name}" } + let!(:result) { reference_filter("See #{reference}", project: project) } + + it 'points to referenced project issues page' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(project.namespace, + project, + label_name: group_label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')). + to match /background-color: #00ff00/ + end + + it 'has valid link text' do + expect(result.css('a').first.text).to eq group_label.name + end + + it 'has valid text' do + expect(result.text).to eq "See #{group_label.name}" + end + + it 'ignores invalid IDs on the referenced label' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp end end end diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb index 274258a045c..275010c1a2c 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do include FilterSpecHelper - let(:project) { create(:project, :public) } + let(:project) { create(:empty_project, :public) } let(:merge) { create(:merge_request, source_project: project) } it 'requires project context' do @@ -86,23 +86,97 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do end end - context 'cross-project reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, :public, namespace: namespace) } - let(:merge) { create(:merge_request, source_project: project2) } - let(:reference) { merge.to_reference(project) } + context 'cross-project / cross-namespace complete reference' do + let(:project2) { create(:empty_project, :public) } + let(:merge) { create(:merge_request, source_project: project2) } + let(:reference) { "#{project2.path_with_namespace}!#{merge.iid}" } it 'links to a valid reference' do doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')). to eq urls.namespace_project_merge_request_url(project2.namespace, - project, merge) + project2, merge) end - it 'links with adjacent text' do + it 'link has valid text' do doc = reference_filter("Merge (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + + expect(doc.css('a').first.text).to eq(reference) + end + + it 'has valid text' do + doc = reference_filter("Merge (#{reference}.)") + + expect(doc.text).to eq("Merge (#{reference}.)") + end + + it 'ignores invalid merge IDs on the referenced project' do + exp = act = "Merge #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project / same-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let!(:merge) { create(:merge_request, source_project: project2) } + let(:reference) { "#{project2.path_with_namespace}!#{merge.iid}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_merge_request_url(project2.namespace, + project2, merge) + end + + it 'link has valid text' do + doc = reference_filter("Merge (#{reference}.)") + + expect(doc.css('a').first.text).to eq("#{project2.path}!#{merge.iid}") + end + + it 'has valid text' do + doc = reference_filter("Merge (#{reference}.)") + + expect(doc.text).to eq("Merge (#{project2.path}!#{merge.iid}.)") + end + + it 'ignores invalid merge IDs on the referenced project' do + exp = act = "Merge #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project shorthand reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let!(:merge) { create(:merge_request, source_project: project2) } + let(:reference) { "#{project2.path}!#{merge.iid}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_merge_request_url(project2.namespace, + project2, merge) + end + + it 'link has valid text' do + doc = reference_filter("Merge (#{reference}.)") + + expect(doc.css('a').first.text).to eq("#{project2.path}!#{merge.iid}") + end + + it 'has valid text' do + doc = reference_filter("Merge (#{reference}.)") + + expect(doc.text).to eq("Merge (#{project2.path}!#{merge.iid}.)") end it 'ignores invalid merge IDs on the referenced project' do diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index 7419863d848..73b5edb99b3 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -148,13 +148,51 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do end end - describe 'cross project milestone references' do - let(:another_project) { create(:empty_project, :public) } - let(:project_path) { another_project.path_with_namespace } - let(:milestone) { create(:milestone, project: another_project) } - let(:reference) { milestone.to_reference(project) } + describe 'cross-project / cross-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:another_project) { create(:empty_project, :public, namespace: namespace) } + let(:milestone) { create(:milestone, project: another_project) } + let(:reference) { "#{another_project.path_with_namespace}%#{milestone.iid}" } + let!(:result) { reference_filter("See #{reference}") } - let!(:result) { reference_filter("See #{reference}") } + it 'points to referenced project milestone page' do + expect(result.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(another_project.namespace, + another_project, + milestone) + end + + it 'link has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.css('a').first.text). + to eq("#{milestone.name} in #{another_project.path_with_namespace}") + end + + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text). + to eq("See (#{milestone.name} in #{another_project.path_with_namespace}.)") + end + + it 'escapes the name attribute' do + allow_any_instance_of(Milestone).to receive(:title).and_return(%{"></a>whatever<a title="}) + + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.text). + to eq "#{milestone.name} in #{another_project.path_with_namespace}" + end + end + + describe 'cross-project / same-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:another_project) { create(:empty_project, :public, namespace: namespace) } + let(:milestone) { create(:milestone, project: another_project) } + let(:reference) { "#{another_project.path_with_namespace}%#{milestone.iid}" } + let!(:result) { reference_filter("See #{reference}") } it 'points to referenced project milestone page' do expect(result.css('a').first.attr('href')).to eq urls. @@ -163,14 +201,66 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do milestone) end - it 'contains cross project content' do - expect(result.css('a').first.text).to eq "#{milestone.name} in #{project_path}" + it 'link has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.css('a').first.text). + to eq("#{milestone.name} in #{another_project.path}") + end + + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text). + to eq("See (#{milestone.name} in #{another_project.path}.)") end it 'escapes the name attribute' do allow_any_instance_of(Milestone).to receive(:title).and_return(%{"></a>whatever<a title="}) + + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.text). + to eq "#{milestone.name} in #{another_project.path}" + end + end + + describe 'cross project shorthand reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:another_project) { create(:empty_project, :public, namespace: namespace) } + let(:milestone) { create(:milestone, project: another_project) } + let(:reference) { "#{another_project.path}%#{milestone.iid}" } + let!(:result) { reference_filter("See #{reference}") } + + it 'points to referenced project milestone page' do + expect(result.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(another_project.namespace, + another_project, + milestone) + end + + it 'link has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.css('a').first.text). + to eq("#{milestone.name} in #{another_project.path}") + end + + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text). + to eq("See (#{milestone.name} in #{another_project.path}.)") + end + + it 'escapes the name attribute' do + allow_any_instance_of(Milestone).to receive(:title).and_return(%{"></a>whatever<a title="}) + doc = reference_filter("See #{reference}") - expect(doc.css('a').first.text).to eq "#{milestone.name} in #{project_path}" + + expect(doc.css('a').first.text). + to eq "#{milestone.name} in #{another_project.path}" end end end diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb index 9b92d1a3926..e036514d283 100644 --- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb @@ -79,11 +79,11 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do end end - context 'cross-project reference' do - let(:namespace) { create(:namespace, name: 'cross-reference') } + context 'cross-project / cross-namespace complete reference' do + let(:namespace) { create(:namespace) } let(:project2) { create(:empty_project, :public, namespace: namespace) } - let(:snippet) { create(:project_snippet, project: project2) } - let(:reference) { snippet.to_reference(project) } + let!(:snippet) { create(:project_snippet, project: project2) } + let(:reference) { "#{project2.path_with_namespace}$#{snippet.id}" } it 'links to a valid reference' do doc = reference_filter("See #{reference}") @@ -92,9 +92,82 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) end - it 'links with adjacent text' do + it 'link has valid text' do doc = reference_filter("See (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + + expect(doc.css('a').first.text).to eql(reference) + end + + it 'has valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text).to eql("See (#{reference}.)") + end + + it 'ignores invalid snippet IDs on the referenced project' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project / same-namespace complete reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let!(:snippet) { create(:project_snippet, project: project2) } + let(:reference) { "#{project2.path_with_namespace}$#{snippet.id}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) + end + + it 'link has valid text' do + doc = reference_filter("See (#{project2.path}$#{snippet.id}.)") + + expect(doc.css('a').first.text).to eql("#{project2.path}$#{snippet.id}") + end + + it 'has valid text' do + doc = reference_filter("See (#{project2.path}$#{snippet.id}.)") + + expect(doc.text).to eql("See (#{project2.path}$#{snippet.id}.)") + end + + it 'ignores invalid snippet IDs on the referenced project' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'cross-project shorthand reference' do + let(:namespace) { create(:namespace) } + let(:project) { create(:empty_project, :public, namespace: namespace) } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let!(:snippet) { create(:project_snippet, project: project2) } + let(:reference) { "#{project2.path}$#{snippet.id}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) + end + + it 'link has valid text' do + doc = reference_filter("See (#{project2.path}$#{snippet.id}.)") + + expect(doc.css('a').first.text).to eql("#{project2.path}$#{snippet.id}") + end + + it 'has valid text' do + doc = reference_filter("See (#{project2.path}$#{snippet.id}.)") + + expect(doc.text).to eql("See (#{project2.path}$#{snippet.id}.)") end it 'ignores invalid snippet IDs on the referenced project' do diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb new file mode 100644 index 00000000000..619ecbcba67 --- /dev/null +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Canceled do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'canceled' } + end + + describe '#label' do + it { expect(subject.label).to eq 'canceled' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_canceled' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: canceled' } + end +end diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb new file mode 100644 index 00000000000..157302c65a8 --- /dev/null +++ b/spec/lib/gitlab/ci/status/created_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Created do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'created' } + end + + describe '#label' do + it { expect(subject.label).to eq 'created' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_created' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: created' } + end +end diff --git a/spec/lib/gitlab/ci/status/extended_spec.rb b/spec/lib/gitlab/ci/status/extended_spec.rb new file mode 100644 index 00000000000..120e121aae5 --- /dev/null +++ b/spec/lib/gitlab/ci/status/extended_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Extended do + subject do + Class.new.extend(described_class) + end + + it 'requires subclass to implement matcher' do + expect { subject.matches?(double) } + .to raise_error(NotImplementedError) + end +end diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb new file mode 100644 index 00000000000..0b3cb8168e6 --- /dev/null +++ b/spec/lib/gitlab/ci/status/failed_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Failed do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'failed' } + end + + describe '#label' do + it { expect(subject.label).to eq 'failed' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_failed' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: failed' } + end +end diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb new file mode 100644 index 00000000000..57c901c1202 --- /dev/null +++ b/spec/lib/gitlab/ci/status/pending_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Pending do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'pending' } + end + + describe '#label' do + it { expect(subject.label).to eq 'pending' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_pending' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: pending' } + end +end diff --git a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb new file mode 100644 index 00000000000..21adee3f8e7 --- /dev/null +++ b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Pipeline::Common do + let(:pipeline) { create(:ci_pipeline) } + + subject do + Class.new(Gitlab::Ci::Status::Core) + .new(pipeline).extend(described_class) + end + + it 'does not have action' do + expect(subject).not_to have_action + end + + it 'has details' do + expect(subject).to have_details + end + + it 'links to the pipeline details page' do + expect(subject.details_path) + .to include "pipelines/#{pipeline.id}" + end +end diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb new file mode 100644 index 00000000000..d6243940f2e --- /dev/null +++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Pipeline::Factory do + subject do + described_class.new(pipeline) + end + + let(:status) do + subject.fabricate! + end + + context 'when pipeline has a core status' do + HasStatus::AVAILABLE_STATUSES.each do |core_status| + context "when core status is #{core_status}" do + let(:pipeline) do + create(:ci_pipeline, status: core_status) + end + + it "fabricates a core status #{core_status}" do + expect(status).to be_a( + Gitlab::Ci::Status.const_get(core_status.capitalize)) + end + + it 'extends core status with common pipeline methods' do + expect(status).to have_details + expect(status).not_to have_action + expect(status.details_path) + .to include "pipelines/#{pipeline.id}" + end + end + end + end + + context 'when pipeline has warnings' do + let(:pipeline) do + create(:ci_pipeline, status: :success) + end + + before do + create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline) + end + + it 'fabricates extended "success with warnings" status' do + expect(status) + .to be_a Gitlab::Ci::Status::Pipeline::SuccessWithWarnings + end + + it 'extends core status with common pipeline methods' do + expect(status).to have_details + end + end +end diff --git a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb new file mode 100644 index 00000000000..02e526e3de2 --- /dev/null +++ b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do + subject do + described_class.new(double('status')) + end + + describe '#test' do + it { expect(subject.text).to eq 'passed' } + end + + describe '#label' do + it { expect(subject.label).to eq 'passed with warnings' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_warning' } + end + + describe '.matches?' do + context 'when pipeline is successful' do + let(:pipeline) do + create(:ci_pipeline, status: :success) + end + + context 'when pipeline has warnings' do + before do + allow(pipeline).to receive(:has_warnings?).and_return(true) + end + + it 'is a correct match' do + expect(described_class.matches?(pipeline)).to eq true + end + end + + context 'when pipeline does not have warnings' do + it 'does not match' do + expect(described_class.matches?(pipeline)).to eq false + end + end + end + + context 'when pipeline is not successful' do + let(:pipeline) do + create(:ci_pipeline, status: :skipped) + end + + context 'when pipeline has warnings' do + before do + allow(pipeline).to receive(:has_warnings?).and_return(true) + end + + it 'does not match' do + expect(described_class.matches?(pipeline)).to eq false + end + end + + context 'when pipeline does not have warnings' do + it 'does not match' do + expect(described_class.matches?(pipeline)).to eq false + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb new file mode 100644 index 00000000000..c023f1872cc --- /dev/null +++ b/spec/lib/gitlab/ci/status/running_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Running do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'running' } + end + + describe '#label' do + it { expect(subject.label).to eq 'running' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_running' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: running' } + end +end diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb new file mode 100644 index 00000000000..d4f7f4b3b70 --- /dev/null +++ b/spec/lib/gitlab/ci/status/skipped_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Skipped do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'skipped' } + end + + describe '#label' do + it { expect(subject.label).to eq 'skipped' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_skipped' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: skipped' } + end +end diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb new file mode 100644 index 00000000000..9e261a3aa5f --- /dev/null +++ b/spec/lib/gitlab/ci/status/success_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Success do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'passed' } + end + + describe '#label' do + it { expect(subject.label).to eq 'passed' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_success' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: passed' } + end +end diff --git a/spec/lib/gitlab/cycle_analytics/permissions_spec.rb b/spec/lib/gitlab/cycle_analytics/permissions_spec.rb index dc4f7dc69db..2d85e712db0 100644 --- a/spec/lib/gitlab/cycle_analytics/permissions_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/permissions_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::CycleAnalytics::Permissions do - let(:project) { create(:empty_project) } + let(:project) { create(:empty_project, public_builds: false) } let(:user) { create(:user) } subject { described_class.get(user: user, project: project) } diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb index 6b3dfebd85d..d619e401897 100644 --- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb @@ -64,7 +64,7 @@ describe Gitlab::Gfm::ReferenceRewriter do context 'description with project labels' do let!(:label) { create(:label, id: 123, name: 'test', project: old_project) } - let(:project_ref) { old_project.to_reference } + let(:project_ref) { old_project.to_reference(new_project) } context 'label referenced by id' do let(:text) { '#1 and ~123' } @@ -80,7 +80,7 @@ describe Gitlab::Gfm::ReferenceRewriter do context 'description with group labels' do let(:old_group) { create(:group) } let!(:group_label) { create(:group_label, id: 321, name: 'group label', group: old_group) } - let(:project_ref) { old_project.to_reference } + let(:project_ref) { old_project.to_reference(new_project) } before do old_project.update(namespace: old_group) diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 000b9aa6f83..9e027839f59 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -155,7 +155,7 @@ describe Gitlab::GithubImport::Importer, lib: true do message: 'The remote data could not be fully imported.', errors: [ { type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" }, - { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank, Title is too short (minimum is 0 characters)" }, + { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank" }, { type: :wiki, errors: "Gitlab::Shell::Error" }, { type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" } ] diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb index 770e8b0c2f4..1cb02f8e318 100644 --- a/spec/lib/gitlab/import_export/members_mapper_spec.rb +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' describe Gitlab::ImportExport::MembersMapper, services: true do describe 'map members' do - let(:user) { create(:user) } + let(:user) { create(:user, authorized_projects_populated: true) } let(:project) { create(:project, :public, name: 'searchable_project') } - let(:user2) { create(:user) } + let(:user2) { create(:user, authorized_projects_populated: true) } let(:exported_user_id) { 99 } let(:exported_members) do [{ @@ -67,5 +67,12 @@ describe Gitlab::ImportExport::MembersMapper, services: true do expect(ProjectMember.find_by_invite_email('invite@test.com')).not_to be_nil end + + it 'authorizes the users to the project' do + members_mapper.map + + expect(user.authorized_project?(project)).to be true + expect(user2.authorized_project?(project)).to be true + end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 0d2b4920835..3f93d9ddf19 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -404,6 +404,76 @@ describe Ci::Pipeline, models: true do end end + describe '#detailed_status' do + context 'when pipeline is created' do + let(:pipeline) { create(:ci_pipeline, status: :created) } + + it 'returns detailed status for created pipeline' do + expect(pipeline.detailed_status.text).to eq 'created' + end + end + + context 'when pipeline is pending' do + let(:pipeline) { create(:ci_pipeline, status: :pending) } + + it 'returns detailed status for pending pipeline' do + expect(pipeline.detailed_status.text).to eq 'pending' + end + end + + context 'when pipeline is running' do + let(:pipeline) { create(:ci_pipeline, status: :running) } + + it 'returns detailed status for running pipeline' do + expect(pipeline.detailed_status.text).to eq 'running' + end + end + + context 'when pipeline is successful' do + let(:pipeline) { create(:ci_pipeline, status: :success) } + + it 'returns detailed status for successful pipeline' do + expect(pipeline.detailed_status.text).to eq 'passed' + end + end + + context 'when pipeline is failed' do + let(:pipeline) { create(:ci_pipeline, status: :failed) } + + it 'returns detailed status for failed pipeline' do + expect(pipeline.detailed_status.text).to eq 'failed' + end + end + + context 'when pipeline is canceled' do + let(:pipeline) { create(:ci_pipeline, status: :canceled) } + + it 'returns detailed status for canceled pipeline' do + expect(pipeline.detailed_status.text).to eq 'canceled' + end + end + + context 'when pipeline is skipped' do + let(:pipeline) { create(:ci_pipeline, status: :skipped) } + + it 'returns detailed status for skipped pipeline' do + expect(pipeline.detailed_status.text).to eq 'skipped' + end + end + + context 'when pipeline is successful but with warnings' do + let(:pipeline) { create(:ci_pipeline, status: :success) } + + before do + create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline) + end + + it 'retruns detailed status for successful pipeline with warnings' do + expect(pipeline.detailed_status.label).to eq 'passed with warnings' + end + end + end + describe '#cancelable?' do %i[created running pending].each do |status0| context "when there is a build #{status0}" do diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index 4e7833c3162..bee9f714849 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -5,6 +5,13 @@ describe Ci::Variable, models: true do let(:secret_value) { 'secret' } + it { is_expected.to validate_presence_of(:key) } + it { is_expected.to validate_uniqueness_of(:key).scoped_to(:gl_project_id) } + it { is_expected.to validate_length_of(:key).is_at_most(255) } + it { is_expected.to allow_value('foo').for(:key) } + it { is_expected.not_to allow_value('foo bar').for(:key) } + it { is_expected.not_to allow_value('foo/bar').for(:key) } + before :each do subject.value = secret_value end diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index c41359b55a3..d89d4342dea 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -45,7 +45,7 @@ describe CommitRange, models: true do end describe '#to_reference' do - let(:cross) { create(:project) } + let(:cross) { create(:empty_project, namespace: project.namespace) } it 'returns a String reference to the object' do expect(range.to_reference).to eq "#{full_sha_from}...#{full_sha_to}" @@ -56,12 +56,12 @@ describe CommitRange, models: true do end it 'supports a cross-project reference' do - expect(range.to_reference(cross)).to eq "#{project.to_reference}@#{full_sha_from}...#{full_sha_to}" + expect(range.to_reference(cross)).to eq "#{project.path}@#{full_sha_from}...#{full_sha_to}" end end describe '#reference_link_text' do - let(:cross) { create(:project) } + let(:cross) { create(:empty_project, namespace: project.namespace) } it 'returns a String reference to the object' do expect(range.reference_link_text).to eq "#{sha_from}...#{sha_to}" @@ -72,7 +72,7 @@ describe CommitRange, models: true do end it 'supports a cross-project reference' do - expect(range.reference_link_text(cross)).to eq "#{project.to_reference}@#{sha_from}...#{sha_to}" + expect(range.reference_link_text(cross)).to eq "#{project.path}@#{sha_from}...#{sha_to}" end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 7194c20d3bf..eb482c7f913 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -34,24 +34,30 @@ describe Commit, models: true do end describe '#to_reference' do + let(:project) { create(:project, path: 'sample-project') } + let(:commit) { project.commit } + it 'returns a String reference to the object' do expect(commit.to_reference).to eq commit.id end it 'supports a cross-project reference' do - cross = double('project') - expect(commit.to_reference(cross)).to eq "#{project.to_reference}@#{commit.id}" + another_project = build(:project, name: 'another-project', namespace: project.namespace) + expect(commit.to_reference(another_project)).to eq "sample-project@#{commit.id}" end end describe '#reference_link_text' do + let(:project) { create(:project, path: 'sample-project') } + let(:commit) { project.commit } + it 'returns a String reference to the object' do expect(commit.reference_link_text).to eq commit.short_id end it 'supports a cross-project reference' do - cross = double('project') - expect(commit.reference_link_text(cross)).to eq "#{project.to_reference}@#{commit.short_id}" + another_project = build(:project, name: 'another-project', namespace: project.namespace) + expect(commit.reference_link_text(another_project)).to eq "sample-project@#{commit.short_id}" end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 6f84bffe046..4fa06a8c60a 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -35,7 +35,7 @@ describe Issue, "Issuable" do it { is_expected.to validate_presence_of(:iid) } it { is_expected.to validate_presence_of(:author) } it { is_expected.to validate_presence_of(:title) } - it { is_expected.to validate_length_of(:title).is_at_least(0).is_at_most(255) } + it { is_expected.to validate_length_of(:title).is_at_most(255) } end describe "Scope" do diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb index 2a67c60b978..bc32fadd391 100644 --- a/spec/models/discussion_spec.rb +++ b/spec/models/discussion_spec.rb @@ -521,6 +521,15 @@ describe Discussion, model: true do end end + describe "#first_note_to_resolve" do + it "returns the first not that still needs to be resolved" do + allow(first_note).to receive(:to_be_resolved?).and_return(false) + allow(second_note).to receive(:to_be_resolved?).and_return(true) + + expect(subject.first_note_to_resolve).to eq(second_note) + end + end + describe "#collapsed?" do context "when a diff discussion" do before do diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index d06665197db..c8170164898 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -13,9 +13,9 @@ describe Environment, models: true do it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } - it { is_expected.to validate_length_of(:name).is_within(0..255) } + it { is_expected.to validate_length_of(:name).is_at_most(255) } - it { is_expected.to validate_length_of(:external_url).is_within(0..255) } + it { is_expected.to validate_length_of(:external_url).is_at_most(255) } # To circumvent a not null violation of the name column: # https://github.com/thoughtbot/shoulda-matchers/issues/336 diff --git a/spec/models/group_label_spec.rb b/spec/models/group_label_spec.rb index 2369658bf78..668aa6fb357 100644 --- a/spec/models/group_label_spec.rb +++ b/spec/models/group_label_spec.rb @@ -37,6 +37,16 @@ describe GroupLabel, models: true do end end + context 'cross-project' do + let(:namespace) { build_stubbed(:namespace) } + let(:source_project) { build_stubbed(:empty_project, name: 'project-1', namespace: namespace) } + let(:target_project) { build_stubbed(:empty_project, name: 'project-2', namespace: namespace) } + + it 'returns a String reference to the object' do + expect(label.to_reference(source_project, target_project)).to eq %(project-1~#{label.id}) + end + end + context 'using invalid format' do it 'raises error' do expect { label.to_reference(format: :invalid) } diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 89e93dce8c5..24e216329a9 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -43,14 +43,16 @@ describe Issue, models: true do end describe '#to_reference' do + let(:project) { build(:empty_project, name: 'sample-project') } + let(:issue) { build(:issue, iid: 1, project: project) } + it 'returns a String reference to the object' do - expect(subject.to_reference).to eq "##{subject.iid}" + expect(issue.to_reference).to eq "#1" end it 'supports a cross-project reference' do - cross = double('project') - expect(subject.to_reference(cross)). - to eq "#{subject.project.to_reference}##{subject.iid}" + another_project = build(:project, name: 'another-project', namespace: project.namespace) + expect(issue.to_reference(another_project)).to eq "sample-project#1" end end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 90731f55470..2a33d819138 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -7,9 +7,13 @@ describe Key, models: true do describe "Validation" do it { is_expected.to validate_presence_of(:title) } + it { is_expected.to validate_length_of(:title).is_at_most(255) } + it { is_expected.to validate_presence_of(:key) } - it { is_expected.to validate_length_of(:title).is_within(0..255) } - it { is_expected.to validate_length_of(:key).is_within(0..5000) } + it { is_expected.to validate_length_of(:key).is_at_most(5000) } + it { is_expected.to allow_value('ssh-foo').for(:key) } + it { is_expected.to allow_value('ecdsa-foo').for(:key) } + it { is_expected.not_to allow_value('foo-bar').for(:key) } end describe "Methods" do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 2cc818af6c7..8b730be91fd 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -142,13 +142,16 @@ describe MergeRequest, models: true do end describe '#to_reference' do + let(:project) { build(:empty_project, name: 'sample-project') } + let(:merge_request) { build(:merge_request, target_project: project, iid: 1) } + it 'returns a String reference to the object' do - expect(subject.to_reference).to eq "!#{subject.iid}" + expect(merge_request.to_reference).to eq "!1" end it 'supports a cross-project reference' do - cross = double('project') - expect(subject.to_reference(cross)).to eq "#{subject.source_project.to_reference}!#{subject.iid}" + another_project = build(:project, name: 'another-project', namespace: project.namespace) + expect(merge_request.to_reference(another_project)).to eq "sample-project!1" end end @@ -1124,6 +1127,46 @@ describe MergeRequest, models: true do allow(subject).to receive(:diff_discussions).and_return([first_discussion, second_discussion, third_discussion]) end + describe '#resolvable_discussions' do + before do + allow(first_discussion).to receive(:to_be_resolved?).and_return(true) + allow(second_discussion).to receive(:to_be_resolved?).and_return(false) + allow(third_discussion).to receive(:to_be_resolved?).and_return(false) + end + + it 'includes only discussions that need to be resolved' do + expect(subject.resolvable_discussions).to eq([first_discussion]) + end + end + + describe '#discussions_can_be_resolved_by? user' do + let(:user) { build(:user) } + + context 'all discussions can be resolved by the user' do + before do + allow(first_discussion).to receive(:can_resolve?).with(user).and_return(true) + allow(second_discussion).to receive(:can_resolve?).with(user).and_return(true) + allow(third_discussion).to receive(:can_resolve?).with(user).and_return(true) + end + + it 'allows a user to resolve the discussions' do + expect(subject.discussions_can_be_resolved_by?(user)).to be(true) + end + end + + context 'one discussion cannot be resolved by the user' do + before do + allow(first_discussion).to receive(:can_resolve?).with(user).and_return(true) + allow(second_discussion).to receive(:can_resolve?).with(user).and_return(true) + allow(third_discussion).to receive(:can_resolve?).with(user).and_return(false) + end + + it 'allows a user to resolve the discussions' do + expect(subject.discussions_can_be_resolved_by?(user)).to be(false) + end + end + end + describe "#discussions_resolvable?" do context "when all discussions are unresolvable" do before do diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index a4bfe851dfb..0cc2efae5f9 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -246,4 +246,18 @@ describe Milestone, models: true do end end end + + describe '#to_reference' do + let(:project) { build(:empty_project, name: 'sample-project') } + let(:milestone) { build(:milestone, iid: 1, project: project) } + + it 'returns a String reference to the object' do + expect(milestone.to_reference).to eq "%1" + end + + it 'supports a cross-project reference' do + another_project = build(:project, name: 'another-project', namespace: project.namespace) + expect(milestone.to_reference(another_project)).to eq "sample-project%1" + end + end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 431b3e4435f..ba0ed4a3603 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -4,11 +4,18 @@ describe Namespace, models: true do let!(:namespace) { create(:namespace) } it { is_expected.to have_many :projects } - it { is_expected.to validate_presence_of :name } + + it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name) } - it { is_expected.to validate_presence_of :path } + it { is_expected.to validate_length_of(:name).is_at_most(255) } + + it { is_expected.to validate_length_of(:description).is_at_most(255) } + + it { is_expected.to validate_presence_of(:path) } it { is_expected.to validate_uniqueness_of(:path) } - it { is_expected.to validate_presence_of :owner } + it { is_expected.to validate_length_of(:path).is_at_most(255) } + + it { is_expected.to validate_presence_of(:owner) } describe "Mass assignment" do end diff --git a/spec/models/project_label_spec.rb b/spec/models/project_label_spec.rb index 18c9d449ee5..4d538cac007 100644 --- a/spec/models/project_label_spec.rb +++ b/spec/models/project_label_spec.rb @@ -105,14 +105,14 @@ describe ProjectLabel, models: true do context 'using name' do it 'returns cross reference with label name' do expect(label.to_reference(project, format: :name)) - .to eq %Q(#{label.project.to_reference}~"#{label.name}") + .to eq %Q(#{label.project.path_with_namespace}~"#{label.name}") end end context 'using id' do it 'returns cross reference with label id' do expect(label.to_reference(project, format: :id)) - .to eq %Q(#{label.project.to_reference}~#{label.id}) + .to eq %Q(#{label.project.path_with_namespace}~#{label.id}) end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 8abcce42ce0..4d57330ed1c 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -131,14 +131,18 @@ describe Project, models: true do it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) } - it { is_expected.to validate_length_of(:name).is_within(0..255) } + it { is_expected.to validate_length_of(:name).is_at_most(255) } it { is_expected.to validate_presence_of(:path) } it { is_expected.to validate_uniqueness_of(:path).scoped_to(:namespace_id) } - it { is_expected.to validate_length_of(:path).is_within(0..255) } - it { is_expected.to validate_length_of(:description).is_within(0..2000) } + it { is_expected.to validate_length_of(:path).is_at_most(255) } + + it { is_expected.to validate_length_of(:description).is_at_most(2000) } + it { is_expected.to validate_presence_of(:creator) } + it { is_expected.to validate_presence_of(:namespace) } + it { is_expected.to validate_presence_of(:repository_storage) } it 'does not allow new projects beyond user limits' do @@ -258,10 +262,70 @@ describe Project, models: true do end describe '#to_reference' do - let(:project) { create(:empty_project) } + let(:owner) { create(:user, name: 'Gitlab') } + let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) } + let(:project) { create(:empty_project, path: 'sample-project', namespace: namespace) } - it 'returns a String reference to the object' do - expect(project.to_reference).to eq project.path_with_namespace + context 'when nil argument' do + it 'returns nil' do + expect(project.to_reference).to be_nil + end + end + + context 'when same project argument' do + it 'returns nil' do + expect(project.to_reference(project)).to be_nil + end + end + + context 'when cross namespace project argument' do + let(:another_namespace_project) { create(:empty_project, name: 'another-project') } + + it 'returns complete path to the project' do + expect(project.to_reference(another_namespace_project)).to eq 'sample-namespace/sample-project' + end + end + + context 'when same namespace / cross-project argument' do + let(:another_project) { create(:empty_project, namespace: namespace) } + + it 'returns complete path to the project' do + expect(project.to_reference(another_project)).to eq 'sample-project' + end + end + end + + describe '#to_human_reference' do + let(:owner) { create(:user, name: 'Gitlab') } + let(:namespace) { create(:namespace, name: 'Sample namespace', owner: owner) } + let(:project) { create(:empty_project, name: 'Sample project', namespace: namespace) } + + context 'when nil argument' do + it 'returns nil' do + expect(project.to_human_reference).to be_nil + end + end + + context 'when same project argument' do + it 'returns nil' do + expect(project.to_human_reference(project)).to be_nil + end + end + + context 'when cross namespace project argument' do + let(:another_namespace_project) { create(:empty_project, name: 'another-project') } + + it 'returns complete name with namespace of the project' do + expect(project.to_human_reference(another_namespace_project)).to eq 'Gitlab / Sample project' + end + end + + context 'when same namespace / cross-project argument' do + let(:another_project) { create(:empty_project, namespace: namespace) } + + it 'returns name of the project' do + expect(project.to_human_reference(another_project)).to eq 'Sample project' + end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index b797d19161d..d9b0e63eeb6 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -768,7 +768,6 @@ describe Repository, models: true do expect(repository).not_to receive(:expire_root_ref_cache) expect(repository).not_to receive(:expire_emptiness_caches) expect(repository).to receive(:expire_branches_cache) - expect(repository).to receive(:expire_has_visible_content_cache) repository.update_branch_with_hooks(user, 'new-feature') { new_rev } end @@ -786,7 +785,6 @@ describe Repository, models: true do expect(empty_repository).to receive(:expire_root_ref_cache) expect(empty_repository).to receive(:expire_emptiness_caches) expect(empty_repository).to receive(:expire_branches_cache) - expect(empty_repository).to receive(:expire_has_visible_content_cache) empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!', 'Updates file content', 'master', false) @@ -829,15 +827,6 @@ describe Repository, models: true do expect(subject).to eq(true) end - - it 'caches the output' do - expect(repository).to receive(:branch_count). - once. - and_return(3) - - repository.has_visible_content? - repository.has_visible_content? - end end end @@ -918,20 +907,6 @@ describe Repository, models: true do end end - describe '#expire_has_visible_content_cache' do - it 'expires the visible content cache' do - repository.has_visible_content? - - expect(repository).to receive(:branch_count). - once. - and_return(0) - - repository.expire_has_visible_content_cache - - expect(repository.has_visible_content?).to eq(false) - end - end - describe '#expire_branch_cache' do # This method is private but we need it for testing purposes. Sadly there's # no other proper way of testing caching operations. @@ -967,7 +942,6 @@ describe Repository, models: true do allow(repository).to receive(:empty?).and_return(true) expect(cache).to receive(:expire).with(:empty?) - expect(repository).to receive(:expire_has_visible_content_cache) repository.expire_emptiness_caches end @@ -976,7 +950,6 @@ describe Repository, models: true do allow(repository).to receive(:empty?).and_return(false) expect(cache).not_to receive(:expire).with(:empty?) - expect(repository).not_to receive(:expire_has_visible_content_cache) repository.expire_emptiness_caches end @@ -1204,7 +1177,7 @@ describe Repository, models: true do describe '#after_create_branch' do it 'flushes the visible content cache' do - expect(repository).to receive(:expire_has_visible_content_cache) + expect(repository).to receive(:expire_branches_cache) repository.after_create_branch end @@ -1212,7 +1185,7 @@ describe Repository, models: true do describe '#after_remove_branch' do it 'flushes the visible content cache' do - expect(repository).to receive(:expire_has_visible_content_cache) + expect(repository).to receive(:expire_branches_cache) repository.after_remove_branch end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index f62f6bacbaa..7425a897769 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -23,9 +23,9 @@ describe Snippet, models: true do it { is_expected.to validate_presence_of(:author) } it { is_expected.to validate_presence_of(:title) } - it { is_expected.to validate_length_of(:title).is_within(0..255) } + it { is_expected.to validate_length_of(:title).is_at_most(255) } - it { is_expected.to validate_length_of(:file_name).is_within(0..255) } + it { is_expected.to validate_length_of(:file_name).is_at_most(255) } it { is_expected.to validate_presence_of(:content) } @@ -33,16 +33,51 @@ describe Snippet, models: true do end describe '#to_reference' do + context 'when snippet belongs to a project' do + let(:project) { build(:empty_project, name: 'sample-project') } + let(:snippet) { build(:snippet, id: 1, project: project) } + + it 'returns a String reference to the object' do + expect(snippet.to_reference).to eq "$1" + end + + it 'supports a cross-project reference' do + another_project = build(:project, name: 'another-project', namespace: project.namespace) + expect(snippet.to_reference(another_project)).to eq "sample-project$1" + end + end + + context 'when snippet does not belong to a project' do + let(:snippet) { build(:snippet, id: 1, project: nil) } + + it 'returns a String reference to the object' do + expect(snippet.to_reference).to eq "$1" + end + + it 'still returns shortest reference when project arg present' do + another_project = build(:project, name: 'another-project') + expect(snippet.to_reference(another_project)).to eq "$1" + end + end + end + + describe '#file_name' do let(:project) { create(:empty_project) } - let(:snippet) { create(:snippet, project: project) } - it 'returns a String reference to the object' do - expect(snippet.to_reference).to eq "$#{snippet.id}" + context 'file_name is nil' do + let(:snippet) { create(:snippet, project: project, file_name: nil) } + + it 'returns an empty string' do + expect(snippet.file_name).to eq '' + end end - it 'supports a cross-project reference' do - cross = double('project') - expect(snippet.to_reference(cross)).to eq "#{project.to_reference}$#{snippet.id}" + context 'file_name is not nil' do + let(:snippet) { create(:snippet, project: project, file_name: 'foo.txt') } + + it 'returns the file_name' do + expect(snippet.file_name).to eq 'foo.txt' + end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 2244803f90c..bad6ed9e146 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -79,7 +79,7 @@ describe User, models: true do it { is_expected.to allow_value(0).for(:projects_limit) } it { is_expected.not_to allow_value(-1).for(:projects_limit) } - it { is_expected.to validate_length_of(:bio).is_within(0..255) } + it { is_expected.to validate_length_of(:bio).is_at_most(255) } it_behaves_like 'an object with email-formated attributes', :email do subject { build(:user) } diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index b49e4f3a8bc..eeab9827d99 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -111,14 +111,36 @@ describe ProjectPolicy, models: true do context 'guests' do let(:current_user) { guest } + let(:reporter_public_build_permissions) do + reporter_permissions - [:read_build, :read_pipeline] + end + it do is_expected.to include(*guest_permissions) - is_expected.not_to include(*reporter_permissions) + is_expected.not_to include(*reporter_public_build_permissions) is_expected.not_to include(*team_member_reporter_permissions) is_expected.not_to include(*developer_permissions) is_expected.not_to include(*master_permissions) is_expected.not_to include(*owner_permissions) end + + context 'public builds enabled' do + it do + is_expected.to include(*guest_permissions) + is_expected.to include(:read_build, :read_pipeline) + end + end + + context 'public builds disabled' do + before do + project.update(public_builds: false) + end + + it do + is_expected.to include(*guest_permissions) + is_expected.not_to include(:read_build, :read_pipeline) + end + end end context 'reporter' do diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 0ea991b18b8..7be7acebb19 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -5,7 +5,7 @@ describe API::Builds, api: true do let(:user) { create(:user) } let(:api_user) { user } - let!(:project) { create(:project, creator_id: user.id) } + let!(:project) { create(:project, creator_id: user.id, public_builds: false) } let!(:developer) { create(:project_member, :developer, user: user, project: project) } let(:reporter) { create(:project_member, :reporter, project: project) } let(:guest) { create(:project_member, :guest, project: project) } diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 5fa7299044e..aabab8e6ae6 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -75,7 +75,6 @@ describe API::DeployKeys, api: true do expect(response).to have_http_status(400) expect(json_response['message']['key']).to eq([ 'can\'t be blank', - 'is too short (minimum is 0 characters)', 'is invalid' ]) end @@ -85,8 +84,7 @@ describe API::DeployKeys, api: true do expect(response).to have_http_status(400) expect(json_response['message']['title']).to eq([ - 'can\'t be blank', - 'is too short (minimum is 0 characters)' + 'can\'t be blank' ]) end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 5700f800c2e..553983575c4 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -692,6 +692,32 @@ describe API::Issues, api: true do ]) end + context 'resolving issues in a merge request' do + let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first } + let(:merge_request) { discussion.noteable } + let(:project) { merge_request.source_project } + before do + project.team << [user, :master] + post api("/projects/#{project.id}/issues", user), + title: 'New Issue', + merge_request_for_resolving_discussions: merge_request.iid + end + + it 'creates a new project issue' do + expect(response).to have_http_status(:created) + end + + it 'resolves the discussions in a merge request' do + discussion.first_note.reload + + expect(discussion.resolved?).to be(true) + end + + it 'assigns a description to the issue mentioning the merge request' do + expect(json_response['description']).to include(merge_request.to_reference) + end + end + context 'with due date' do it 'creates a new project issue' do due_date = 2.weeks.from_now.strftime('%Y-%m-%d') diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb index f5e0fdcda2d..e0368e6001f 100644 --- a/spec/requests/projects/cycle_analytics_events_spec.rb +++ b/spec/requests/projects/cycle_analytics_events_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe 'cycle analytics events' do let(:user) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:project, public_builds: false) } let(:issue) { create(:issue, project: project, created_at: 2.days.ago) } describe 'GET /:namespace/:project/cycle_analytics/events/issues' do diff --git a/spec/services/discussions/resolve_service_spec.rb b/spec/services/discussions/resolve_service_spec.rb new file mode 100644 index 00000000000..12c3cdf28c6 --- /dev/null +++ b/spec/services/discussions/resolve_service_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe Discussions::ResolveService do + describe '#execute' do + let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first } + let(:project) { merge_request.project } + let(:merge_request) { discussion.noteable } + let(:user) { create(:user) } + let(:service) { described_class.new(discussion.noteable.project, user, merge_request: merge_request) } + + before do + project.team << [user, :master] + end + + it "doesn't resolve discussions the user can't resolve" do + expect(discussion).to receive(:can_resolve?).with(user).and_return(false) + + service.execute(discussion) + + expect(discussion.resolved?).to be(false) + end + + it 'resolves the discussion' do + service.execute(discussion) + + expect(discussion.resolved?).to be(true) + end + + it 'executes the notification service' do + expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService).to receive(:execute).with(discussion.noteable) + + service.execute(discussion) + end + + it 'adds a system note to the discussion' do + issue = create(:issue, project: project) + + expect(SystemNoteService).to receive(:discussion_continued_in_issue).with(discussion, project, user, issue) + service = described_class.new(project, user, merge_request: merge_request, follow_up_issue: issue) + service.execute(discussion) + end + + it 'can resolve multiple discussions at once' do + other_discussion = Discussion.for_diff_notes([create(:diff_note_on_merge_request, noteable: discussion.noteable, project: discussion.noteable.source_project)]).first + + service.execute([discussion, other_discussion]) + + expect(discussion.resolved?).to be(true) + expect(other_discussion.resolved?).to be(true) + end + end +end diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb new file mode 100644 index 00000000000..4cfba35c830 --- /dev/null +++ b/spec/services/issues/build_service_spec.rb @@ -0,0 +1,130 @@ +require 'spec_helper.rb' + +describe Issues::BuildService, services: true do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.team << [user, :developer] + end + + context 'for discussions in a merge request' do + let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) } + let(:issue) { described_class.new(project, user, merge_request_for_resolving_discussions: merge_request).execute } + + def position_on_line(line_number) + Gitlab::Diff::Position.new( + old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: line_number, + diff_refs: merge_request.diff_refs + ) + end + + describe '#items_for_discussions' do + it 'has an item for each discussion' do + create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.source_project, position: position_on_line(13)) + service = described_class.new(project, user, merge_request_for_resolving_discussions: merge_request) + + service.execute + + expect(service.items_for_discussions.size).to eq(2) + end + end + + describe '#item_for_discussion' do + let(:service) { described_class.new(project, user, merge_request_for_resolving_discussions: merge_request) } + + it 'mentions the author of the note' do + discussion = Discussion.new([create(:diff_note_on_merge_request, author: create(:user, username: 'author'))]) + expect(service.item_for_discussion(discussion)).to include('@author') + end + + it 'wraps the note in a blockquote' do + note_text = "This is a string\n"\ + ">>>\n"\ + "with a blockquote\n"\ + "> That has a quote\n"\ + ">>>\n" + note_result = "This is a string\n"\ + "> with a blockquote\n"\ + "> > That has a quote\n" + discussion = Discussion.new([create(:diff_note_on_merge_request, note: note_text)]) + expect(service.item_for_discussion(discussion)).to include(">>>\n#{note_result}\n>>>") + end + end + + describe '#execute' do + it 'has the merge request reference in the title' do + expect(issue.title).to include(merge_request.title) + end + + it 'has the reference of the merge request in the description' do + expect(issue.description).to include(merge_request.to_reference) + end + + it 'does not assign title when a title was given' do + issue = described_class.new(project, user, + merge_request_for_resolving_discussions: merge_request, + title: 'What an issue').execute + + expect(issue.title).to eq('What an issue') + end + + it 'does not assign description when a description was given' do + issue = described_class.new(project, user, + merge_request_for_resolving_discussions: merge_request, + description: 'Fix at your earliest conveignance').execute + + expect(issue.description).to eq('Fix at your earliest conveignance') + end + + describe 'with multiple discussions' do + before do + create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.target_project, position: position_on_line(15)) + end + + it 'mentions all the authors in the description' do + authors = merge_request.diff_discussions.map(&:author) + + expect(issue.description).to include(*authors.map(&:to_reference)) + end + + it 'has a link for each unresolved discussion in the description' do + notes = merge_request.diff_discussions.map(&:first_note) + links = notes.map { |note| Gitlab::UrlBuilder.build(note) } + + expect(issue.description).to include(*links) + end + + it 'mentions additional notes' do + create_list(:diff_note_on_merge_request, 2, noteable: merge_request, project: merge_request.target_project, position: position_on_line(15)) + + expect(issue.description).to include('(+2 comments)') + end + end + end + end + + context 'For a merge request without discussions' do + let(:merge_request) { create(:merge_request, source_project: project) } + + describe '#execute' do + it 'mentions the merge request in the description' do + issue = described_class.new(project, user, merge_request_for_resolving_discussions: merge_request).execute + + expect(issue.description).to include("Review the conversation in #{merge_request.to_reference}") + end + end + end + + describe '#execute' do + it 'builds a new issues with given params' do + issue = described_class.new(project, user, title: 'Issue #1', description: 'Issue description').execute + + expect(issue.title).to eq('Issue #1') + expect(issue.description).to eq('Issue description') + end + end +end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 5c0331ebe66..8bde61ee336 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -136,5 +136,48 @@ describe Issues::CreateService, services: true do end it_behaves_like 'new issuable record that supports slash commands' + + context 'for a merge request' do + let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first } + let(:merge_request) { discussion.noteable } + let(:project) { merge_request.source_project } + let(:opts) { { merge_request_for_resolving_discussions: merge_request } } + + before do + project.team << [user, :master] + end + + it 'resolves the discussion for the merge request' do + described_class.new(project, user, opts).execute + discussion.first_note.reload + + expect(discussion.resolved?).to be(true) + end + + it 'added a system note to the discussion' do + described_class.new(project, user, opts).execute + + reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first + + expect(reloaded_discussion.last_note.system).to eq(true) + end + + it 'assigns the title and description for the issue' do + issue = described_class.new(project, user, opts).execute + + expect(issue.title).not_to be_nil + expect(issue.description).not_to be_nil + end + + it 'can set nil explicityly to the title and description' do + issue = described_class.new(project, user, + merge_request_for_resolving_discussions: merge_request, + description: nil, + title: nil).execute + + expect(issue.description).to be_nil + expect(issue.title).to be_nil + end + end end end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index c7de0d0c534..db196ed5751 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -189,7 +189,7 @@ describe Issues::MoveService, services: true do it 'rewrites references using a cross reference to old project' do expect(new_note.note) - .to eq "Note with reference to merge request #{old_project.to_reference}!1" + .to eq "Note with reference to merge request #{old_project.to_reference(new_project)}!1" end end @@ -217,7 +217,7 @@ describe Issues::MoveService, services: true do it 'rewrites referenced issues creating cross project reference' do expect(new_issue.description) - .to eq "Some description #{old_project.to_reference}#{another_issue.to_reference}" + .to eq "Some description #{another_issue.to_reference(new_project)}" end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 435cfb07292..90b7e62bc6f 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -530,7 +530,7 @@ describe SystemNoteService, services: true do end it 'mentions referenced project' do - expect(subject.note).to include new_project.to_reference + expect(subject.note).to include new_project.path_with_namespace end end @@ -712,4 +712,32 @@ describe SystemNoteService, services: true do end end end + + describe '.discussion_continued_in_issue' do + let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first } + let(:merge_request) { discussion.noteable } + let(:project) { merge_request.source_project } + let(:issue) { create(:issue, project: project) } + let(:user) { create(:user) } + + def reloaded_merge_request + MergeRequest.find(merge_request.id) + end + + before do + project.team << [user, :developer] + end + + it 'creates a new note in the discussion' do + # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded. + expect { SystemNoteService.discussion_continued_in_issue(discussion, project, user, issue) }. + to change { reloaded_merge_request.discussions.first.notes.size }.by(1) + end + + it 'mentions the created issue in the system note' do + note = SystemNoteService.discussion_continued_in_issue(discussion, project, user, issue) + + expect(note.note).to include(issue.to_reference) + end + end end diff --git a/spec/support/matchers/is_within.rb b/spec/support/matchers/is_within.rb deleted file mode 100644 index 0c35fc7e899..00000000000 --- a/spec/support/matchers/is_within.rb +++ /dev/null @@ -1,9 +0,0 @@ -# Extend shoulda-matchers -module Shoulda::Matchers::ActiveModel - class ValidateLengthOfMatcher - # Shortcut for is_at_least and is_at_most - def is_within(range) - is_at_least(range.min) && is_at_most(range.max) - end - end -end diff --git a/spec/workers/pipeline_notification_worker_spec.rb b/spec/workers/pipeline_notification_worker_spec.rb index 739f9b63967..603ae52ed1e 100644 --- a/spec/workers/pipeline_notification_worker_spec.rb +++ b/spec/workers/pipeline_notification_worker_spec.rb @@ -11,7 +11,7 @@ describe PipelineNotificationWorker do status: status) end - let(:project) { create(:project) } + let(:project) { create(:project, public_builds: false) } let(:user) { create(:user) } let(:pusher) { user } let(:watcher) { pusher } |