summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/issue_templates/Bug.md2
-rw-r--r--app/assets/stylesheets/framework.scss3
-rw-r--r--app/assets/stylesheets/framework/awards.scss (renamed from app/assets/stylesheets/pages/awards.scss)0
-rw-r--r--app/assets/stylesheets/framework/blocks.scss9
-rw-r--r--app/assets/stylesheets/framework/broadcast-messages.scss21
-rw-r--r--app/assets/stylesheets/framework/common.scss14
-rw-r--r--app/assets/stylesheets/framework/images.scss (renamed from app/assets/stylesheets/pages/appearances.scss)0
-rw-r--r--app/assets/stylesheets/framework/lists.scss39
-rw-r--r--app/assets/stylesheets/framework/nav.scss10
-rw-r--r--app/assets/stylesheets/framework/tables.scss17
-rw-r--r--app/assets/stylesheets/framework/variables.scss6
-rw-r--r--app/assets/stylesheets/framework/wells.scss13
-rw-r--r--app/assets/stylesheets/pages/admin.scss168
-rw-r--r--app/assets/stylesheets/pages/confirmation.scss32
-rw-r--r--app/assets/stylesheets/pages/dashboard.scss43
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss6
-rw-r--r--app/assets/stylesheets/pages/tags.scss7
-rw-r--r--app/controllers/concerns/merge_requests_action.rb7
-rw-r--r--app/controllers/projects/discussions_controller.rb4
-rw-r--r--app/controllers/projects/issues_controller.rb15
-rw-r--r--app/finders/issuable_finder.rb6
-rw-r--r--app/finders/merge_requests_finder.rb1
-rw-r--r--app/helpers/ci_status_helper.rb25
-rw-r--r--app/helpers/gitlab_markdown_helper.rb2
-rw-r--r--app/helpers/labels_helper.rb9
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/models/ci/variable.rb4
-rw-r--r--app/models/commit.rb22
-rw-r--r--app/models/commit_range.rb17
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/models/concerns/referable.rb13
-rw-r--r--app/models/discussion.rb4
-rw-r--r--app/models/environment.rb2
-rw-r--r--app/models/issue.rb6
-rw-r--r--app/models/key.rb16
-rw-r--r--app/models/label.rb15
-rw-r--r--app/models/member.rb1
-rw-r--r--app/models/merge_request.rb14
-rw-r--r--app/models/milestone.rb13
-rw-r--r--app/models/namespace.rb14
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/project.rb37
-rw-r--r--app/models/repository.rb14
-rw-r--r--app/models/snippet.rb16
-rw-r--r--app/policies/ci/build_policy.rb2
-rw-r--r--app/policies/project_policy.rb8
-rw-r--r--app/services/discussions/base_service.rb4
-rw-r--r--app/services/discussions/resolve_service.rb24
-rw-r--r--app/services/issuable_base_service.rb5
-rw-r--r--app/services/issues/base_service.rb8
-rw-r--r--app/services/issues/build_service.rb50
-rw-r--r--app/services/issues/create_service.rb14
-rw-r--r--app/services/system_note_service.rb8
-rw-r--r--app/views/admin/abuse_reports/index.html.haml10
-rw-r--r--app/views/admin/dashboard/index.html.haml12
-rw-r--r--app/views/admin/users/_user.html.haml8
-rw-r--r--app/views/admin/users/index.html.haml2
-rw-r--r--app/views/devise/confirmations/almost_there.haml7
-rw-r--r--app/views/layouts/nav/_group.html.haml2
-rw-r--r--app/views/projects/buttons/_download.html.haml79
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml7
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml3
-rw-r--r--app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml6
-rw-r--r--app/views/projects/pipelines/_info.html.haml2
-rw-r--r--app/views/projects/pipelines_settings/show.html.haml3
-rw-r--r--app/views/projects/tags/show.html.haml21
-rw-r--r--app/views/shared/issuable/_form.html.haml15
-rw-r--r--app/views/shared/issuable/_nav.html.haml10
-rw-r--r--changelogs/unreleased/23589-open-issue-for-mr.yml5
-rw-r--r--changelogs/unreleased/24733-archived-project-merge-request-count.yml4
-rw-r--r--changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml4
-rw-r--r--changelogs/unreleased/glm-shorthand-reference.yml4
-rw-r--r--changelogs/unreleased/remove-has-visible-content-caching.yml4
-rw-r--r--changelogs/unreleased/zj-guest-reads-public-builds.yml4
-rw-r--r--config/routes/group.rb22
-rw-r--r--doc/administration/custom_hooks.md20
-rw-r--r--doc/api/issues.md3
-rw-r--r--doc/ci/examples/php.md2
-rw-r--r--doc/development/ux_guide/animation.md42
-rw-r--r--doc/development/ux_guide/basics.md15
-rw-r--r--doc/development/ux_guide/copy.md4
-rw-r--r--doc/development/ux_guide/img/animation-dropdown.gifbin0 -> 22483 bytes
-rw-r--r--doc/development/ux_guide/img/animation-hover.gifbin0 -> 247388 bytes
-rw-r--r--doc/development/ux_guide/img/animation-quickupdate.gifbin0 -> 6441 bytes
-rw-r--r--doc/development/ux_guide/index.md17
-rw-r--r--doc/user/markdown.md12
-rw-r--r--doc/user/project/merge_requests/img/preview_issue_for_discussions.pngbin0 -> 178361 bytes
-rw-r--r--doc/user/project/merge_requests/merge_request_discussion_resolution.md21
-rw-r--r--features/steps/shared/project.rb2
-rw-r--r--lib/api/issues.rb29
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb23
-rw-r--r--lib/banzai/filter/commit_range_reference_filter.rb2
-rw-r--r--lib/banzai/filter/commit_reference_filter.rb2
-rw-r--r--lib/banzai/filter/label_reference_filter.rb54
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb20
-rw-r--r--lib/gitlab/ci/status/canceled.rb19
-rw-r--r--lib/gitlab/ci/status/core.rb58
-rw-r--r--lib/gitlab/ci/status/created.rb19
-rw-r--r--lib/gitlab/ci/status/extended.rb11
-rw-r--r--lib/gitlab/ci/status/failed.rb19
-rw-r--r--lib/gitlab/ci/status/pending.rb19
-rw-r--r--lib/gitlab/ci/status/pipeline/common.rb23
-rw-r--r--lib/gitlab/ci/status/pipeline/factory.rb39
-rw-r--r--lib/gitlab/ci/status/pipeline/success_with_warnings.rb31
-rw-r--r--lib/gitlab/ci/status/running.rb19
-rw-r--r--lib/gitlab/ci/status/skipped.rb19
-rw-r--r--lib/gitlab/ci/status/success.rb19
-rwxr-xr-xscripts/lint-doc.sh2
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb60
-rw-r--r--spec/features/groups/labels/edit_spec.rb21
-rw-r--r--spec/features/groups/merge_requests_spec.rb30
-rw-r--r--spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb76
-rw-r--r--spec/features/issues/move_spec.rb2
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb2
-rw-r--r--spec/features/merge_requests/deleted_source_branch_spec.rb3
-rw-r--r--spec/features/projects/guest_navigation_menu_spec.rb4
-rw-r--r--spec/features/security/project/private_access_spec.rb55
-rw-r--r--spec/finders/merge_requests_finder_spec.rb11
-rw-r--r--spec/helpers/labels_helper_spec.rb4
-rw-r--r--spec/lib/banzai/filter/abstract_link_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/commit_range_reference_filter_spec.rb99
-rw-r--r--spec/lib/banzai/filter/commit_reference_filter_spec.rb78
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb108
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb291
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb92
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb108
-rw-r--r--spec/lib/banzai/filter/snippet_reference_filter_spec.rb85
-rw-r--r--spec/lib/gitlab/ci/status/canceled_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/status/created_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/status/extended_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/status/failed_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/status/pending_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/common_spec.rb23
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/factory_spec.rb52
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb65
-rw-r--r--spec/lib/gitlab/ci/status/running_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/status/skipped_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/status/success_spec.rb21
-rw-r--r--spec/lib/gitlab/cycle_analytics/permissions_spec.rb2
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb4
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/members_mapper_spec.rb11
-rw-r--r--spec/models/ci/pipeline_spec.rb70
-rw-r--r--spec/models/ci/variable_spec.rb7
-rw-r--r--spec/models/commit_range_spec.rb8
-rw-r--r--spec/models/commit_spec.rb14
-rw-r--r--spec/models/concerns/issuable_spec.rb2
-rw-r--r--spec/models/discussion_spec.rb9
-rw-r--r--spec/models/environment_spec.rb4
-rw-r--r--spec/models/group_label_spec.rb10
-rw-r--r--spec/models/issue_spec.rb10
-rw-r--r--spec/models/key_spec.rb8
-rw-r--r--spec/models/merge_request_spec.rb49
-rw-r--r--spec/models/milestone_spec.rb14
-rw-r--r--spec/models/namespace_spec.rb13
-rw-r--r--spec/models/project_label_spec.rb4
-rw-r--r--spec/models/project_spec.rb76
-rw-r--r--spec/models/repository_spec.rb31
-rw-r--r--spec/models/snippet_spec.rb51
-rw-r--r--spec/models/user_spec.rb2
-rw-r--r--spec/policies/project_policy_spec.rb24
-rw-r--r--spec/requests/api/builds_spec.rb2
-rw-r--r--spec/requests/api/deploy_keys_spec.rb4
-rw-r--r--spec/requests/api/issues_spec.rb26
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb2
-rw-r--r--spec/services/discussions/resolve_service_spec.rb52
-rw-r--r--spec/services/issues/build_service_spec.rb130
-rw-r--r--spec/services/issues/create_service_spec.rb43
-rw-r--r--spec/services/issues/move_service_spec.rb4
-rw-r--r--spec/services/system_note_service_spec.rb30
-rw-r--r--spec/support/matchers/is_within.rb9
-rw-r--r--spec/workers/pipeline_notification_worker_spec.rb2
172 files changed, 2964 insertions, 794 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/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/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/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 9560e9d518e..c9bee01b9ad 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -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
@@ -356,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/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/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/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/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/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/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/development/ux_guide/animation.md b/doc/development/ux_guide/animation.md
new file mode 100644
index 00000000000..daeb15460c2
--- /dev/null
+++ b/doc/development/ux_guide/animation.md
@@ -0,0 +1,42 @@
+# Animation
+
+Motion is a tool to help convey important relationships, changes or transitions between elements. It should be used sparingly and intentionally, highlighting the right elements at the right moment.
+
+## Timings
+
+The longer distance an object travel, the timing should be longer for the animation. However, when in doubt, we should avoid large, full screen animations.
+
+Subtle animations, or objects leaving the screen should take **100-200 milliseconds**. Objects entering the screen, or motion we want to use to direct user attention can take between **200-400 milliseconds**. We should avoid animations of longer than 400 milliseconds as they will make the experience appear sluggish. If a specific animation feels like it will need more than 400 milliseconds, revisit the animation to see if there is a simpler, easier, shorter animation to implement.
+
+## Easing
+
+Easing specifies the rate of change of a parameter over time (see [easings.net](http://easings.net/)). Adding an easing curve will make the motion feel more natural. Being consistent with the easing curves will make the whole experience feel more cohesive and connected.
+
+* When an object is entering the screen, or transforming the scale, position, or shape, use the **easeOutQuint** curve (`cubic-bezier(0.23, 1, 0.32, 1)`)
+* When an object is leaving the screen, or transforming the opacity or color, no easing curve is needed. It shouldn't _slow down_ as it is exiting the screen, as that draws attention on the leaving object, where we don't want it. Adding easing to opacity and color transitions will make the motion appear less smooth. Therefore, for these cases, motion should just be **linear**.
+
+## Types of animations
+
+### Hover
+
+Interactive elements (links, buttons, etc.) should have a hover state. A subtle animation for this transition adds a polished feel. We should target a `200ms linear` transition for a color hover effect.
+
+View the [interactive example](http://codepen.io/awhildy/full/GNyEvM/) here.
+
+![Hover animation](img/animation-hover.gif)
+
+### Dropdowns
+
+The dropdown menu should feel like it is appearing from the triggering element. Combining a position shift `400ms cubic-bezier(0.23, 1, 0.32, 1)` with a opacity animation `200ms linear` on the second half of the motion achieves this affect.
+
+View the [interactive example](http://codepen.io/awhildy/full/jVLJpb/) here.
+
+![Dropdown animation](img/animation-dropdown.gif)
+
+### Quick update
+
+When information is updating in place, a quick, subtle animation is needed. The previous content should cut out, and the new content should have a quick, `200ms linear` fade in.
+
+![Quick update animation](img/animation-quickupdate.gif)
+
+> TODO: Add guidance for other kinds of animation \ No newline at end of file
diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md
index a29cfa096b2..76b739386a5 100644
--- a/doc/development/ux_guide/basics.md
+++ b/doc/development/ux_guide/basics.md
@@ -5,8 +5,6 @@
* [Typography](#typography)
* [Icons](#icons)
* [Color](#color)
-* [Motion](#motion)
-* [Voice and tone](#voice-and-tone)
---
@@ -61,16 +59,3 @@ GitLab uses Font Awesome icons throughout our interface.
> TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage.
----
-
-## Motion
-
-Motion is a tool to help convey important relationships, changes or transitions between elements. It should be used sparingly and intentionally, highlighting the right elements at the right moment.
-
-> TODO: Determine a more concrete perspective on motion, create consistent easing/timing curves to follow.
-
----
-
-## Voice and tone
-
-The copy for GitLab is clear and direct. We strike a clear balance between professional and friendly. We can empathesize with users (such as celebrating completing all Todos), and remain respectful of the importance of the work. We are that trusted, friendly coworker that is helpful and understanding.
diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md
index b557fb47120..8896d450f14 100644
--- a/doc/development/ux_guide/copy.md
+++ b/doc/development/ux_guide/copy.md
@@ -1,6 +1,8 @@
# Copy
-The copy and messaging is a core part of the experience of GitLab and the conversation with our users. Follow the below conventions throughout GitLab.
+The copy for GitLab is clear and direct. We strike a clear balance between professional and friendly. We can empathesize with users (such as celebrating completing all Todos), and remain respectful of the importance of the work. We are that trusted, friendly coworker that is helpful and understanding.
+
+The copy and messaging is a core part of the experience of GitLab and the conversation with our users. Follow the below conventions throughout GitLab.
>**Note:**
We are currently inconsistent with this guidance. Images below are created to illustrate the point. As this guidance is refined, we will ensure that our experiences align.
diff --git a/doc/development/ux_guide/img/animation-dropdown.gif b/doc/development/ux_guide/img/animation-dropdown.gif
new file mode 100644
index 00000000000..c9b31d26165
--- /dev/null
+++ b/doc/development/ux_guide/img/animation-dropdown.gif
Binary files differ
diff --git a/doc/development/ux_guide/img/animation-hover.gif b/doc/development/ux_guide/img/animation-hover.gif
new file mode 100644
index 00000000000..37ad9c76828
--- /dev/null
+++ b/doc/development/ux_guide/img/animation-hover.gif
Binary files differ
diff --git a/doc/development/ux_guide/img/animation-quickupdate.gif b/doc/development/ux_guide/img/animation-quickupdate.gif
new file mode 100644
index 00000000000..8db70bc3d24
--- /dev/null
+++ b/doc/development/ux_guide/img/animation-quickupdate.gif
Binary files differ
diff --git a/doc/development/ux_guide/index.md b/doc/development/ux_guide/index.md
index 8aed11ebac3..8a849f239dc 100644
--- a/doc/development/ux_guide/index.md
+++ b/doc/development/ux_guide/index.md
@@ -12,7 +12,17 @@ These guiding principles set a solid foundation for our design system, and shoul
---
### [Basics](basics.md)
-The basic ingredients of our experience establish our personality and feel. This section includes details about typography, color, and motion.
+The basic ingredients of our experience establish our personality and feel. This section includes details about typography, iconography, and color.
+
+---
+
+### [Animation](animation.md)
+Guidance on the timing, curving and motion for GitLab.
+
+---
+
+### [Copy](copy.md)
+Conventions on text and messaging within labels, buttons, and other components.
---
@@ -26,11 +36,6 @@ The GitLab experience is broken apart into several surfaces. Each of these surfa
---
-### [Copy](copy.md)
-Conventions on text and messaging within labels, buttons, and other components.
-
----
-
### [Features](features.md)
The previous building blocks are combined into complete features in the GitLab UX. Examples include our navigation, filters, search results, and empty states.
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
new file mode 100644
index 00000000000..9fdd387676c
--- /dev/null
+++ b/doc/user/project/merge_requests/img/preview_issue_for_discussions.png
Binary files differ
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/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 }