summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.csscomb.json32
-rw-r--r--.gitlab-ci.yml1
-rw-r--r--.scss-lint.yml2
-rw-r--r--CHANGELOG17
-rw-r--r--CONTRIBUTING.md23
-rw-r--r--app/assets/javascripts/api.js.coffee2
-rw-r--r--app/assets/javascripts/application.js.coffee11
-rw-r--r--app/assets/javascripts/issuable_context.js.coffee28
-rw-r--r--app/assets/javascripts/issue.js.coffee25
-rw-r--r--app/assets/javascripts/labels_select.js.coffee12
-rw-r--r--app/assets/javascripts/sidebar.js.coffee1
-rw-r--r--app/assets/stylesheets/framework/common.scss6
-rw-r--r--app/assets/stylesheets/framework/filters.scss8
-rw-r--r--app/assets/stylesheets/framework/fonts.scss4
-rw-r--r--app/assets/stylesheets/framework/header.scss5
-rw-r--r--app/assets/stylesheets/framework/nav.scss37
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss142
-rw-r--r--app/assets/stylesheets/notify.scss10
-rw-r--r--app/assets/stylesheets/pages/labels.scss7
-rw-r--r--app/controllers/concerns/global_milestones.rb1
-rw-r--r--app/controllers/dashboard/application_controller.rb6
-rw-r--r--app/controllers/dashboard/labels_controller.rb9
-rw-r--r--app/controllers/dashboard/milestones_controller.rb15
-rw-r--r--app/controllers/dashboard_controller.rb29
-rw-r--r--app/controllers/groups/milestones_controller.rb6
-rw-r--r--app/helpers/labels_helper.rb2
-rw-r--r--app/helpers/milestones_helper.rb2
-rw-r--r--app/models/commit.rb2
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/models/issue.rb3
-rw-r--r--app/models/label.rb8
-rw-r--r--app/models/milestone.rb2
-rw-r--r--app/models/project.rb2
-rw-r--r--app/services/issues/move_service.rb5
-rw-r--r--app/services/notification_service.rb12
-rw-r--r--app/views/dashboard/issues.html.haml2
-rw-r--r--app/views/doorkeeper/applications/index.html.haml2
-rw-r--r--app/views/groups/issues.html.haml2
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml7
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/issues/index.html.haml2
-rw-r--r--app/views/projects/labels/_label.html.haml2
-rw-r--r--app/views/projects/tags/_download.html.haml2
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--app/views/shared/issuable/_filter.html.haml8
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml1
-rw-r--r--app/views/shared/issuable/_participants.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml6
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--config/routes.rb15
-rw-r--r--doc/monitoring/performance/grafana_configuration.md118
-rw-r--r--doc/monitoring/performance/img/grafana_dashboard_dropdown.pngbin0 -> 29419 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_dashboard_import.pngbin0 -> 40974 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_data_source_configuration.pngbin0 -> 53402 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_data_source_empty.pngbin0 -> 44058 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_save_icon.pngbin0 -> 16024 bytes
-rw-r--r--doc/monitoring/performance/introduction.md3
-rw-r--r--doc/raketasks/backup_restore.md3
-rw-r--r--doc/update/README.md1
-rw-r--r--doc/update/restore_after_failure.md83
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb4
-rw-r--r--lib/gitlab/email/receiver.rb14
-rw-r--r--lib/tasks/gitlab/backup.rake34
-rw-r--r--lib/tasks/gitlab/db.rake35
-rw-r--r--spec/controllers/groups_controller_spec.rb59
-rw-r--r--spec/features/dashboard_milestones_spec.rb29
-rw-r--r--spec/features/issues/update_issues_spec.rb117
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb10
-rw-r--r--spec/models/issue_spec.rb5
-rw-r--r--spec/services/issues/move_service_spec.rb27
-rw-r--r--spec/services/issues/update_service_spec.rb7
-rw-r--r--spec/services/notification_service_spec.rb88
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb40
75 files changed, 962 insertions, 255 deletions
diff --git a/.csscomb.json b/.csscomb.json
index e353e6a63d0..741cc1488b5 100644
--- a/.csscomb.json
+++ b/.csscomb.json
@@ -1,16 +1,20 @@
{
- "always-semicolon": true,
- "color-case": "lower",
- "block-indent": " ",
- "color-shorthand": true,
- "element-case": "lower",
- "space-before-colon": "",
- "space-after-colon": " ",
- "space-before-combinator": " ",
- "space-after-combinator": " ",
- "space-between-declarations": "\n",
- "space-before-opening-brace": " ",
- "space-after-opening-brace": "\n",
- "space-before-closing-brace": "\n",
- "unitless-zero": true
+ "exclude": [
+ "app/assets/stylesheets/framework/tw_bootstrap_variables.scss",
+ "app/assets/stylesheets/framework/fonts.scss"
+ ],
+ "always-semicolon": true,
+ "color-case": "lower",
+ "block-indent": " ",
+ "color-shorthand": true,
+ "element-case": "lower",
+ "space-before-colon": "",
+ "space-after-colon": " ",
+ "space-before-combinator": " ",
+ "space-after-combinator": " ",
+ "space-between-declarations": "\n",
+ "space-before-opening-brace": " ",
+ "space-after-opening-brace": "\n",
+ "space-before-closing-brace": "\n",
+ "unitless-zero": true
}
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2ad63548d78..53f115c92c8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -128,7 +128,6 @@ scss-lint:
- bundle exec rake scss_lint
tags:
- ruby
- allow_failure: true
brakeman:
stage: test
diff --git a/.scss-lint.yml b/.scss-lint.yml
index e350b2073c3..937d3407b60 100644
--- a/.scss-lint.yml
+++ b/.scss-lint.yml
@@ -100,7 +100,7 @@ linters:
# Selectors should always use hyphenated-lowercase, rather than camelCase or
# snake_case.
SelectorFormat:
- enabled: true
+ enabled: false
convention: hyphenated_lowercase
# Prefer the shortest shorthand form possible for properties that support it.
diff --git a/CHANGELOG b/CHANGELOG
index 265302b1356..d9be95defd1 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,11 +1,24 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.7.0 (unreleased)
+ - Preserve time notes/comments have been updated at when moving issue
- Make HTTP(s) label consistent on clone bar (Stan Hu)
- Fix avatar stretching by providing a cropping feature
-v 8.6.1 (unreleased)
-
+v 8.6.1
+ - Add option to reload the schema before restoring a database backup. !2807
+ - Display navigation controls on mobile. !3214
+ - Fixed bug where participants would not work correctly on merge requests. !3329
+ - Fix sorting issues by votes on the groups issues page results in SQL errors. !3333
+ - Restrict notifications for confidential issues. !3334
+ - Do not allow to move issue if it has not been persisted. !3340
+ - Add a confirmation step before deleting an issuable. !3341
+ - Fixes issue with signin button overflowing on mobile. !3342
+ - Auto collapses the navigation sidebar when resizing. !3343
+ - Fix build dependencies, when the dependency is a string. !3344
+ - Shows error messages when trying to create label in dropdown menu. !3345
+ - Fixes issue with assign milestone not loading milestone list. !3346
+ - Fix an issue causing the Dashboard/Milestones page to be blank. !3348
v 8.6.0
- Add ability to move issue to another project
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7540fa1afcc..511336f384c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -16,6 +16,7 @@
- [Issue tracker guidelines](#issue-tracker-guidelines)
- [Issue weight](#issue-weight)
- [Regression issues](#regression-issues)
+ - [Technical debt](#technical-debt)
- [Merge requests](#merge-requests)
- [Merge request guidelines](#merge-request-guidelines)
- [Merge request description format](#merge-request-description-format)
@@ -242,6 +243,28 @@ addressed.
[8.3 Regressions]: https://gitlab.com/gitlab-org/gitlab-ce/issues/4127
[update the notes]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/pro-tips.md#update-the-regression-issue
+### Technical debt
+
+In order to track things that can be improved in GitLab's codebase, we created
+the ~"technical debt" label in [GitLab's issue tracker][ce-tracker].
+
+This label should be added to issues that describe things that can be improved,
+shortcuts that have been taken, code that needs refactoring, features that need
+additional attention, and all other things that have been left behind due to
+high velocity of development.
+
+Everyone can create an issue, though you may need to ask for adding a specific
+label, if you do not have permissions to do it by yourself. Additional labels
+can be combined with the `technical debt` label, to make it easier to schedule
+the improvements for a release.
+
+Issues tagged with the `technical debt` label have the same priority like issues
+that describe a new feature to be introduced in GitLab, and should be scheduled
+for a release by the appropriate person.
+
+Make sure to mention the merge request that the `technical debt` issue is
+associated with in the description of the issue.
+
## Merge requests
We welcome merge requests with fixes and improvements to GitLab code, tests,
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
index 2ddf8612db3..f3ed9a66715 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -74,6 +74,8 @@
dataType: "json"
).done (label) ->
callback(label)
+ .error (message) ->
+ callback(message.responseJSON)
# Return group projects list. Filtered by query
groupProjects: (group_id, query, callback) ->
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 293e0c3bb34..f01c67e9474 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -219,13 +219,20 @@ $ ->
$this = $(this)
$this.attr 'value', $this.val()
+ $sidebarGutterToggle = $('.js-sidebar-toggle')
+ $navIconToggle = $('.toggle-nav-collapse')
+
$(document)
.off 'breakpoint:change'
.on 'breakpoint:change', (e, breakpoint) ->
if breakpoint is 'sm' or breakpoint is 'xs'
- $gutterIcon = $('.js-sidebar-toggle').find('i')
+ $gutterIcon = $sidebarGutterToggle.find('i')
if $gutterIcon.hasClass('fa-angle-double-right')
- $gutterIcon.closest('a').trigger('click')
+ $sidebarGutterToggle.trigger('click')
+
+ $navIcon = $navIconToggle.find('.fa')
+ if $navIcon.hasClass('fa-angle-left')
+ $navIconToggle.trigger('click')
$(document)
.off 'click', '.js-sidebar-toggle'
diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee
index e52b73f94f6..d6d09b36d8d 100644
--- a/app/assets/javascripts/issuable_context.js.coffee
+++ b/app/assets/javascripts/issuable_context.js.coffee
@@ -1,7 +1,7 @@
-#= require jquery.waitforimages
-
class @IssuableContext
constructor: ->
+ @initParticipants()
+
new UsersSelect()
$('select.select2').select2({width: 'resolve', dropdownAutoWidth: true})
@@ -17,3 +17,27 @@ class @IssuableContext
block.find('.js-select2').select2("open")
$(".right-sidebar").niceScroll()
+
+ initParticipants: ->
+ _this = @
+ $(document).on "click", ".js-participants-more", @toggleHiddenParticipants
+
+ $(".js-participants-author").each (i) ->
+ if i >= _this.PARTICIPANTS_ROW_COUNT
+ $(@)
+ .addClass "js-participants-hidden"
+ .hide()
+
+ toggleHiddenParticipants: (e) ->
+ e.preventDefault()
+
+ currentText = $(this).text().trim()
+ lessText = $(this).data("less-text")
+ originalText = $(this).data("original-text")
+
+ if currentText is originalText
+ $(this).text(lessText)
+ else
+ $(this).text(originalText)
+
+ $(".js-participants-hidden").toggle()
diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee
index f50df1f5ea3..d663e34871c 100644
--- a/app/assets/javascripts/issue.js.coffee
+++ b/app/assets/javascripts/issue.js.coffee
@@ -7,7 +7,6 @@ class @Issue
# Prevent duplicate event bindings
@disableTaskList()
@fixAffixScroll()
- @initParticipants()
if $('a.btn-close').length
@initTaskList()
@initIssueBtnEventListeners()
@@ -85,27 +84,3 @@ class @Issue
type: 'PATCH'
url: $('form.js-issuable-update').attr('action')
data: patchData
-
- initParticipants: ->
- _this = @
- $(document).on "click", ".js-participants-more", @toggleHiddenParticipants
-
- $(".js-participants-author").each (i) ->
- if i >= _this.PARTICIPANTS_ROW_COUNT
- $(@)
- .addClass "js-participants-hidden"
- .hide()
-
- toggleHiddenParticipants: (e) ->
- e.preventDefault()
-
- currentText = $(this).text().trim()
- lessText = $(this).data("less-text")
- originalText = $(this).data("original-text")
-
- if currentText is originalText
- $(this).text(lessText)
- else
- $(this).text(originalText)
-
- $(".js-participants-hidden").toggle()
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
index 4a0c18a99a6..f3cb1e3bc09 100644
--- a/app/assets/javascripts/labels_select.js.coffee
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -14,6 +14,9 @@ class @LabelsSelect
defaultLabel = $dropdown.data('default-label')
if newLabelField.length
+ $newLabelError = $dropdown.parent().find('.js-label-error')
+ $newLabelError.hide()
+
$('.suggest-colors-dropdown a').on 'click', (e) ->
e.preventDefault()
e.stopPropagation()
@@ -27,6 +30,7 @@ class @LabelsSelect
e.stopPropagation()
if newLabelField.val() isnt '' and newColorField.val() isnt ''
+ $newLabelError.hide()
$('.js-new-label-btn').disable()
# Create new label with API
@@ -35,7 +39,13 @@ class @LabelsSelect
color: newColorField.val()
}, (label) ->
$('.js-new-label-btn').enable()
- $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
+
+ if label.message?
+ $newLabelError
+ .text label.message
+ .show()
+ else
+ $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
$dropdown.glDropdown(
data: (term, callback) ->
diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee
index eea3f5ee910..860d4f438d0 100644
--- a/app/assets/javascripts/sidebar.js.coffee
+++ b/app/assets/javascripts/sidebar.js.coffee
@@ -4,7 +4,6 @@ expanded = 'page-sidebar-expanded'
toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded")
- $('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
$('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
$.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index bc03c2180be..8625817fdab 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -292,8 +292,11 @@ table {
}
.btn-sign-in {
- margin-top: 10px;
text-shadow: none;
+
+ @media (min-width: $screen-sm-min) {
+ margin-top: 11px;
+ }
}
.side-filters {
@@ -375,7 +378,6 @@ table {
position: absolute;
top: 0;
right: 0;
- width: 250px !important;
visibility: hidden;
}
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 40a508c1ebc..b05c5df1bd8 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -11,3 +11,11 @@
}
}
}
+
+@media (max-width: $screen-xs-max) {
+ .filter-item {
+ display: block;
+ margin: 0 0 10px;
+ }
+}
+
diff --git a/app/assets/stylesheets/framework/fonts.scss b/app/assets/stylesheets/framework/fonts.scss
index 7a946109e3a..5f9685bc71a 100644
--- a/app/assets/stylesheets/framework/fonts.scss
+++ b/app/assets/stylesheets/framework/fonts.scss
@@ -1,3 +1,7 @@
+// Disabling "SpaceAfterPropertyColon" linter because the linter doesn't like
+// the way the `src` property is formatted in this file.
+// scss-lint:disable SpaceAfterPropertyColon
+
/* latin-ext */
@font-face {
font-family: 'Source Sans Pro';
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 71a7ecab8ef..6a68bb5c115 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -70,6 +70,11 @@ header {
.header-content {
height: $header-height;
+ padding-right: 20px;
+
+ @media (min-width: $screen-sm-min) {
+ padding-right: 0;
+ }
.title {
margin: 0;
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 5f4ce87b085..95bdd6d1ea3 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -102,6 +102,10 @@
display: inline-block;
}
+ .icon-label {
+ display: none;
+ }
+
input {
height: 34px;
display: inline-block;
@@ -124,9 +128,38 @@
}
}
- /* Hide on extra small devices (phones) */
@media (max-width: $screen-xs-max) {
- display: none;
+ padding-bottom: 0;
+
+ .btn, form, .dropdown, .dropdown-menu-toggle, .form-control {
+ margin: 0 0 10px;
+ display: block;
+ width: 100%;
+ }
+
+ form {
+ display: block;
+ height: auto;
+
+ input {
+ width: 100%;
+ margin: 0 0 10px;
+ }
+ }
+
+ .input-short {
+ width: 100%;
+ }
+
+ .icon-label {
+ display: inline-block;
+ }
+
+ // Applies on /dashboard/issues
+ .project-item-select-holder {
+ display: block;
+ margin: 0;
+ }
}
/* Small devices (tablets, 768px and lower) */
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index be05db58c40..9d188317783 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -1,3 +1,10 @@
+#logo {
+ z-index: 2;
+ position: absolute;
+ width: 58px;
+ cursor: pointer;
+}
+
.page-with-sidebar {
padding-top: $header-height;
transition-duration: .3s;
@@ -18,28 +25,10 @@
position: absolute;
left: 0;
}
-
- #logo {
- z-index: 2;
- position: absolute;
- width: 58px;
- cursor: pointer;
- }
-
- &.right-sidebar-expanded {
- /* Extra small devices (phones, less than 768px) */
- /* No media query since this is the default in Bootstrap */
- padding-right: 0;
- /* Small devices (tablets, 768px and up) */
- @media (min-width: $screen-sm-min) {
- padding-right: $gutter_width;
- }
-
- }
}
.sidebar-wrapper {
- z-index: 999;
+ z-index: 1000;
background: $background-color;
}
@@ -202,53 +191,27 @@
}
}
-@mixin expanded-sidebar {
- padding-left: $sidebar_collapsed_width;
-
- @media (min-width: $screen-md-min) {
- padding-left: $sidebar_width;
- }
-
- &.right-sidebar-collapsed {
- /* Extra small devices (phones, less than 768px) */
- padding-right: 0;
- /* Small devices (tablets, 768px and up) */
- @media (min-width: $screen-sm-min) {
- padding-right: $sidebar_collapsed_width;
- }
- }
-
- .sidebar-wrapper {
- width: $sidebar_width;
-
- .nav-sidebar {
- width: $sidebar_width;
- }
-
- .nav-sidebar li a{
- width: 230px;
+.collapse-nav a {
+ width: $sidebar_width;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ font-size: 13px;
+ background: transparent;
+ height: 40px;
+ text-align: center;
+ line-height: 40px;
+ transition-duration: .3s;
+ outline: none;
- &.back-link {
- i {
- opacity: 0;
- }
- }
- }
+ &:hover {
+ text-decoration: none;
}
}
-@mixin collapsed-sidebar {
+.page-sidebar-collapsed {
padding-left: $sidebar_collapsed_width;
- &.right-sidebar-collapsed {
- /* Extra small devices (phones, less than 768px) */
- padding-right: 0;
- /* Small devices (tablets, 768px and up) */
- @media (min-width: $screen-sm-min) {
- padding-right: $sidebar_collapsed_width;
- }
- }
-
.sidebar-wrapper {
width: $sidebar_collapsed_width;
@@ -293,35 +256,48 @@
}
}
-.collapse-nav a {
- width: $sidebar_width;
- position: fixed;
- bottom: 0;
- left: 0;
- font-size: 13px;
- background: transparent;
- height: 40px;
- text-align: center;
- line-height: 40px;
- transition-duration: .3s;
- outline: none;
-}
+.page-sidebar-expanded {
+ padding-left: $sidebar_collapsed_width;
+
+ @media (min-width: $screen-md-min) {
+ padding-left: $sidebar_width;
+ }
+
+ .sidebar-wrapper {
+ width: $sidebar_width;
+
+ .nav-sidebar {
+ width: $sidebar_width;
+ }
+
+ .nav-sidebar li a {
+ width: 230px;
-.collapse-nav a:hover {
- text-decoration: none;
- background: #f2f6f7;
+ &.back-link {
+ i {
+ opacity: 0;
+ }
+ }
+ }
+ }
}
-.page-sidebar-collapsed {
- /* Extra small devices (phones, less than 768px) */
- @include collapsed-sidebar;
+.right-sidebar-collapsed {
padding-right: 0;
- /* Small devices (tablets, 768px and up) */
+
@media (min-width: $screen-sm-min) {
- @include collapsed-sidebar;
+ padding-right: $sidebar_collapsed_width;
}
}
-.page-sidebar-expanded {
- @include expanded-sidebar;
+.right-sidebar-expanded {
+ padding-right: 0;
+
+ @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+ padding-right: $sidebar_collapsed_width;
+ }
+
+ @media (min-width: $screen-md-min) {
+ padding-right: $gutter_width;
+ }
}
diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss
index f1d42f80f56..0a13a7e0b54 100644
--- a/app/assets/stylesheets/notify.scss
+++ b/app/assets/stylesheets/notify.scss
@@ -3,12 +3,12 @@ img {
height: auto;
}
p.details {
- font-style:italic;
- color:#777
+ font-style: italic;
+ color: #777
}
.footer p {
- font-size:small;
- color:#777
+ font-size: small;
+ color: #777
}
pre.commit-message {
white-space: pre-wrap;
@@ -20,5 +20,5 @@ pre.commit-message {
color: #090;
}
.file-stats .deleted-file {
- color: #B00;
+ color: #b00;
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 61ee34b695e..3c13573c8fe 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -45,3 +45,10 @@
.label-subscription {
display: inline-block;
}
+
+.dropdown-labels-error {
+ padding: 5px 10px;
+ margin-bottom: 10px;
+ background-color: $gl-danger;
+ color: $white-light;
+}
diff --git a/app/controllers/concerns/global_milestones.rb b/app/controllers/concerns/global_milestones.rb
index 54ea1e454fc..5c503c5b698 100644
--- a/app/controllers/concerns/global_milestones.rb
+++ b/app/controllers/concerns/global_milestones.rb
@@ -6,7 +6,6 @@ module GlobalMilestones
@milestones = MilestonesFinder.new.execute(@projects, params)
@milestones = GlobalMilestone.build_collection(@milestones)
@milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
- @milestones = Kaminari.paginate_array(@milestones).page(params[:page])
end
def milestone
diff --git a/app/controllers/dashboard/application_controller.rb b/app/controllers/dashboard/application_controller.rb
index 962ea38d6c9..9d3d1c23c28 100644
--- a/app/controllers/dashboard/application_controller.rb
+++ b/app/controllers/dashboard/application_controller.rb
@@ -1,3 +1,9 @@
class Dashboard::ApplicationController < ApplicationController
layout 'dashboard'
+
+ private
+
+ def projects
+ @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
+ end
end
diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb
new file mode 100644
index 00000000000..23a4ef21ea2
--- /dev/null
+++ b/app/controllers/dashboard/labels_controller.rb
@@ -0,0 +1,9 @@
+class Dashboard::LabelsController < Dashboard::ApplicationController
+ def index
+ labels = Label.where(project_id: projects).select(:title, :color).uniq(:title)
+
+ respond_to do |format|
+ format.json { render json: labels }
+ end
+ end
+end
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 2bdce0f8a00..fa9c6c054f0 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -2,18 +2,19 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
include GlobalMilestones
before_action :projects
- before_action :milestones, only: [:index]
before_action :milestone, only: [:show]
def index
+ respond_to do |format|
+ format.html do
+ @milestones = Kaminari.paginate_array(milestones).page(params[:page])
+ end
+ format.json do
+ render json: milestones
+ end
+ end
end
def show
end
-
- private
-
- def projects
- @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
- end
end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index b538c7d1608..1dce4a21729 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -3,7 +3,7 @@ class DashboardController < Dashboard::ApplicationController
include MergeRequestsAction
before_action :event_filter, only: :activity
- before_action :projects, only: [:issues, :merge_requests, :labels, :milestones]
+ before_action :projects, only: [:issues, :merge_requests]
respond_to :html
@@ -20,29 +20,6 @@ class DashboardController < Dashboard::ApplicationController
end
end
- def labels
- labels = Label.where(project_id: @projects).select(:title, :color).uniq(:title)
-
- respond_to do |format|
- format.json do
- render json: labels
- end
- end
- end
-
- def milestones
- milestones = Milestone.where(project_id: @projects).active
- epoch = DateTime.parse('1970-01-01')
- grouped_milestones = GlobalMilestone.build_collection(milestones)
- grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
-
- respond_to do |format|
- format.json do
- render json: grouped_milestones
- end
- end
- end
-
protected
def load_events
@@ -57,8 +34,4 @@ class DashboardController < Dashboard::ApplicationController
@events = @event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
-
- def projects
- @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
- end
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 0028f072d5b..b23c3022fb5 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -2,11 +2,15 @@ class Groups::MilestonesController < Groups::ApplicationController
include GlobalMilestones
before_action :group_projects
- before_action :milestones, only: [:index]
before_action :milestone, only: [:show, :update]
before_action :authorize_admin_milestones!, only: [:new, :create, :update]
def index
+ respond_to do |format|
+ format.html do
+ @milestones = Kaminari.paginate_array(milestones).page(params[:page])
+ end
+ end
end
def new
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index e0a8552dfa7..3dded7c2f23 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -114,7 +114,7 @@ module LabelsHelper
if @project
namespace_project_labels_path(@project.namespace, @project, :json)
else
- labels_dashboard_path(:json)
+ dashboard_labels_path(:json)
end
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index c9d8787bd19..87fc2db6901 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -50,7 +50,7 @@ module MilestonesHelper
if @project
namespace_project_milestones_path(@project.namespace, @project, :json)
else
- milestones_dashboard_path(:json)
+ dashboard_milestones_path(:json)
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index ce0b85d50cf..d0dbe009d0d 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -230,7 +230,7 @@ class Commit
end
def revert_message
- %Q{Revert "#{title}"\n\n#{revert_description}}
+ %Q{Revert "#{title.strip}"\n\n#{revert_description}}
end
def reverts_commit?(commit)
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 476e1ce7af0..cf5b2c71675 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -41,7 +41,7 @@ module Issuable
scope :join_project, -> { joins(:project) }
scope :references_project, -> { references(:project) }
- scope :non_archived, -> { join_project.merge(Project.non_archived) }
+ scope :non_archived, -> { join_project.merge(Project.non_archived.only(:where)) }
delegate :name,
:email,
diff --git a/app/models/issue.rb b/app/models/issue.rb
index f32db59ac9f..ed960cb39f4 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -146,7 +146,8 @@ class Issue < ActiveRecord::Base
return false unless user.can?(:admin_issue, to_project)
end
- !moved? && user.can?(:admin_issue, self.project)
+ !moved? && persisted? &&
+ user.can?(:admin_issue, self.project)
end
def to_branch_name
diff --git a/app/models/label.rb b/app/models/label.rb
index f7ffc0b7f36..500d5a35521 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -97,12 +97,12 @@ class Label < ActiveRecord::Base
end
end
- def open_issues_count
- issues.opened.count
+ def open_issues_count(user = nil)
+ issues.visible_to_user(user).opened.count
end
- def closed_issues_count
- issues.closed.count
+ def closed_issues_count(user = nil)
+ issues.visible_to_user(user).closed.count
end
def open_merge_requests_count
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index de7183bf6b4..bbd59eab9ae 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -83,7 +83,7 @@ class Milestone < ActiveRecord::Base
end
def self.upcoming
- self.where('due_date > ?', Time.now).order(due_date: :asc).first
+ self.where('due_date > ?', Time.now).reorder(due_date: :asc).first
end
def to_reference(from_project = nil)
diff --git a/app/models/project.rb b/app/models/project.rb
index 9c8246e8ac0..2285063ab50 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -304,7 +304,7 @@ class Project < ActiveRecord::Base
end
def find_with_namespace(id)
- namespace_path, project_path = id.split('/')
+ namespace_path, project_path = id.split('/', 2)
return nil if !namespace_path || !project_path
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index 3cfbafe1576..a5efb21fab6 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -54,7 +54,8 @@ module Issues
new_note = note.dup
new_params = { project: @new_project, noteable: @new_issue,
note: unfold_references(new_note.note),
- created_at: note.created_at }
+ created_at: note.created_at,
+ updated_at: note.updated_at }
new_note.update(new_params)
end
@@ -78,6 +79,8 @@ module Issues
end
def unfold_references(content)
+ return unless content
+
rewriter = Gitlab::Gfm::ReferenceRewriter.new(content, @old_project,
@current_user)
rewriter.rewrite(@new_project)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 3bdf00a8291..eff0d96f93d 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -162,6 +162,7 @@ class NotificationService
recipients = add_subscribed_users(recipients, note.noteable)
recipients = reject_unsubscribed_users(recipients, note.noteable)
+ recipients = reject_users_without_access(recipients, note.noteable)
recipients.delete(note.author)
recipients = recipients.uniq
@@ -376,6 +377,14 @@ class NotificationService
end
end
+ def reject_users_without_access(recipients, target)
+ return recipients unless target.is_a?(Issue)
+
+ recipients.select do |user|
+ user.can?(:read_issue, target)
+ end
+ end
+
def add_subscribed_users(recipients, target)
return recipients unless target.respond_to? :subscribers
@@ -464,15 +473,16 @@ class NotificationService
end
recipients = reject_unsubscribed_users(recipients, target)
+ recipients = reject_users_without_access(recipients, target)
recipients.delete(current_user)
-
recipients.uniq
end
def build_relabeled_recipients(target, current_user, labels:)
recipients = add_labels_subscribers([], target, labels: labels)
recipients = reject_unsubscribed_users(recipients, target)
+ recipients = reject_users_without_access(recipients, target)
recipients.delete(current_user)
recipients.uniq
end
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index dfa5f80eef8..1eec4db45a0 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -10,6 +10,8 @@
- if current_user
= link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do
= icon('rss')
+ %span.icon-label
+ Subscribe
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues
diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml
index ea0b66c932b..55f4a6f287d 100644
--- a/app/views/doorkeeper/applications/index.html.haml
+++ b/app/views/doorkeeper/applications/index.html.haml
@@ -77,7 +77,7 @@
%em Authorization was granted by entering your username and password in the application.
%td= token.created_at
%td= token.scopes
- %td= render 'delete_form', token: token
+ %td= render 'doorkeeper/authorized_applications/delete_form', token: token
- else
.profile-settings-message.text-center
You don't have any authorized applications
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index b0805593fdc..aea35c50862 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -10,6 +10,8 @@
- if current_user
= link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do
= icon('rss')
+ %span.icon-label
+ Subscribe
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 820743dc8dd..3d16ecb097a 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -17,7 +17,7 @@
.cover-title
%h1
= @group.name
- %span.visibility-icon.has_tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
+ %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
= visibility_level_icon(@group.visibility_level, fw: false)
.cover-desc.username
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index bfa5937cf3f..0f3b8119379 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -6,7 +6,7 @@
= icon('bars')
.navbar-collapse.collapse
- %ul.nav.navbar-nav.pull-right
+ %ul.nav.navbar-nav
%li.hidden-sm.hidden-xs
= render 'layouts/search'
%li.visible-sm.visible-xs
@@ -38,8 +38,9 @@
= link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('sign-out')
- else
- .pull-right
- = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
+ %li
+ %div
+ = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
%h1.title= title
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 514cbfa339d..9b5de17dd3b 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -5,7 +5,7 @@
.cover-title.project-home-desc
%h1
= @project.name
- %span.visibility-icon.has_tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)}
+ %span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)}
= visibility_level_icon(@project.visibility_level, fw: false)
- if @project.description.present?
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 7afea5a5049..88266e21230 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -16,7 +16,7 @@
- else
Name
%b.caret
- %ul.dropdown-menu
+ %ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to namespace_project_branches_path(sort: nil) do
Name
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index fde9304c0f8..efa7642b2dc 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -11,6 +11,8 @@
- if current_user
= link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do
= icon('rss')
+ %span.icon-label
+ Subscribe
= render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project)
- if can? current_user, :create_issue, @project
= link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do
diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml
index 4927d239c1e..0612863296a 100644
--- a/app/views/projects/labels/_label.html.haml
+++ b/app/views/projects/labels/_label.html.haml
@@ -8,7 +8,7 @@
%strong.append-right-20
= link_to_label(label) do
- = pluralize label.open_issues_count, 'open issue'
+ = pluralize label.open_issues_count(current_user), 'open issue'
- if current_user
.label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}}
diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml
index 667057ef2d8..093d1d1bb0f 100644
--- a/app/views/projects/tags/_download.html.haml
+++ b/app/views/projects/tags/_download.html.haml
@@ -6,7 +6,7 @@
%span.caret
%span.sr-only
Select Archive Format
- %ul.col-xs-10.dropdown-menu{ role: 'menu' }
+ %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
%li
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
%i.fa.fa-download
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 66b7ef99650..40c6eb9be45 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -21,7 +21,7 @@
= icon('users')
= number_with_delimiter(group.users.count)
- %span.visibility-icon.has_tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
+ %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
= visibility_level_icon(group.visibility_level, fw: false)
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index ac20f7d1f7e..f91ff0e3694 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -31,18 +31,18 @@
.issues_bulk_update.hide
= form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do
.filter-item.inline
- = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
+ = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
%ul
%li
%a{href: "#", data: {id: "reopen"}} Open
%li
%a{href: "#", data: {id: "close"}} Closed
.filter-item.inline
- = dropdown_tag("Assignee", options: { toggle_class: "js-user-search", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
+ = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
.filter-item.inline
- = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable",
- placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js), use_id: true } })
+ = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
+ placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
= hidden_field_tag 'update[issues_ids]', []
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index 87617315181..186087e8f89 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -27,6 +27,7 @@
.dropdown-page-two
= dropdown_title("Create new label", back: true)
= dropdown_content do
+ .dropdown-labels-error.js-label-error
%input#new_label_color{type: "hidden"}
%input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"}
.dropdown-label-color-preview.js-dropdown-label-color-preview
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
index 3fb409ff727..33a9a494857 100644
--- a/app/views/shared/issuable/_participants.html.haml
+++ b/app/views/shared/issuable/_participants.html.haml
@@ -17,4 +17,4 @@
%a.js-participants-more{href: "#", data: {original_text: "+ #{participants_size - 7} more", less_text: "- show less"}}
+ #{participants_extra} more
:javascript
- Issue.prototype.PARTICIPANTS_ROW_COUNT = #{participants_row};
+ IssuableContext.prototype.PARTICIPANTS_ROW_COUNT = #{participants_row};
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 2b95b19facc..5b2772de3f1 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -33,11 +33,11 @@
.value.bold.hide-collapsed
- if issuable.assignee
= link_to_member(@project, issuable.assignee, size: 32) do
+ - if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
+ %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
+ = icon('exclamation-triangle')
%span.username
= issuable.assignee.to_reference
- - if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
- %a.pull-right.cannot-be-merged{href: '#', data: {toggle: 'tooltip'}, title: 'Not allowed to merge'}
- = icon('exclamation-triangle')
- else
.light None
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 803dd95bc65..53ff8959bc8 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -27,7 +27,7 @@
%span
= icon('star')
= project.star_count
- %span.visibility-icon.has_tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)}
+ %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)}
= visibility_level_icon(project.visibility_level, fw: false)
.title
diff --git a/config/routes.rb b/config/routes.rb
index 90d858d7fc1..6bf22fb4456 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -16,6 +16,18 @@ Rails.application.routes.draw do
end
end
+ # Make the built-in Rails routes available in development, otherwise they'd
+ # get swallowed by the `namespace/project` route matcher below.
+ #
+ # See https://git.io/va79N
+ if Rails.env.development?
+ get '/rails/mailers' => 'rails/mailers#index'
+ get '/rails/mailers/:path' => 'rails/mailers#preview'
+ get '/rails/info/properties' => 'rails/info#properties'
+ get '/rails/info/routes' => 'rails/info#routes'
+ get '/rails/info' => 'rails/info#index'
+ end
+
namespace :ci do
# CI API
Ci::API::API.logger Rails.logger
@@ -351,11 +363,10 @@ Rails.application.routes.draw do
get :issues
get :merge_requests
get :activity
- get :labels
- get :milestones
scope module: :dashboard do
resources :milestones, only: [:index, :show]
+ resources :labels, only: [:index]
resources :groups, only: [:index]
resources :snippets, only: [:index]
diff --git a/doc/monitoring/performance/grafana_configuration.md b/doc/monitoring/performance/grafana_configuration.md
new file mode 100644
index 00000000000..416c9870aa0
--- /dev/null
+++ b/doc/monitoring/performance/grafana_configuration.md
@@ -0,0 +1,118 @@
+# Grafana Configuration
+
+[Grafana](http://grafana.org/) is a tool that allows you to visualize time
+series metrics through graphs and dashboards. It supports several backend
+data stores, including InfluxDB. GitLab writes performance data to InfluxDB
+and Grafana will allow you to query InfluxDB to display useful graphs.
+
+For the easiest installation and configuration, install Grafana on the same
+server as InfluxDB. For larger installations, you may want to split out these
+services.
+
+## Installation
+
+Grafana supplies package repositories (Yum/Apt) for easy installation.
+See [Grafana installation documentation](http://docs.grafana.org/installation/)
+for detailed steps.
+
+> **Note**: Before starting Grafana for the first time, set the admin user
+and password in `/etc/grafana/grafana.ini`. Otherwise, the default password
+will be `admin`.
+
+## Configuration
+
+Login as the admin user. Expand the menu by clicking the Grafana logo in the
+top left corner. Choose 'Data Sources' from the menu. Then, click 'Add new'
+in the top bar.
+
+![Grafana empty data source page](img/grafana_data_source_empty.png)
+
+Fill in the configuration details for the InfluxDB data source. Save and
+Test Connection to ensure the configuration is correct.
+
+- **Name**: InfluxDB
+- **Default**: Checked
+- **Type**: InfluxDB 0.9.x (Even if you're using InfluxDB 0.10.x)
+- **Url**: https://localhost:8086 (Or the remote URL if you've installed InfluxDB
+on a separate server)
+- **Access**: proxy
+- **Database**: gitlab
+- **User**: admin (Or the username configured when setting up InfluxDB)
+- **Password**: The password configured when you set up InfluxDB
+
+![Grafana data source configurations](img/grafana_data_source_configuration.png)
+
+## Apply retention policies and create continuous queries
+
+If you intend to import the GitLab provided Grafana dashboards, you will need
+to copy and run a set of queries against InfluxDB to create the needed data
+sets.
+
+On the InfluxDB server, run the following command, substituting your InfluxDB
+user and password:
+
+```bash
+influxdb --username admin -password super_secret
+```
+
+This will drop you in to an InfluxDB interactive session. Copy the entire
+contents below and paste it in to the interactive session:
+
+```
+CREATE RETENTION POLICY gitlab_30d ON gitlab DURATION 30d REPLICATION 1 DEFAULT
+CREATE RETENTION POLICY seven_days ON gitlab DURATION 7d REPLICATION 1
+CREATE CONTINUOUS QUERY rails_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean", percentile(sql_duration, 95.000) AS "sql_duration_95th", percentile(sql_duration, 99.000) AS "sql_duration_99th", mean(sql_duration) AS "sql_duration_mean", percentile(view_duration, 95.000) AS "view_duration_95th", percentile(view_duration, 99.000) AS "view_duration_99th", mean(view_duration) AS "view_duration_mean" INTO gitlab.seven_days.rails_transaction_timings FROM gitlab.gitlab_30d.rails_transactions GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean", percentile(sql_duration, 95.000) AS "sql_duration_95th", percentile(sql_duration, 99.000) AS "sql_duration_99th", mean(sql_duration) AS "sql_duration_mean", percentile(view_duration, 95.000) AS "view_duration_95th", percentile(view_duration, 99.000) AS "view_duration_99th", mean(view_duration) AS "view_duration_mean" INTO gitlab.seven_days.sidekiq_transaction_timings FROM gitlab.gitlab_30d.sidekiq_transactions GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.rails_transaction_counts FROM gitlab.gitlab_30d.rails_transactions GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.sidekiq_transaction_counts FROM gitlab.gitlab_30d.sidekiq_transactions GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.rails_method_call_timings FROM gitlab.gitlab_30d.rails_method_calls GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.sidekiq_method_call_timings FROM gitlab.gitlab_30d.sidekiq_method_calls GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.rails_method_call_timings_per_method FROM gitlab.gitlab_30d.rails_method_calls GROUP BY time(1m), method END
+CREATE CONTINUOUS QUERY sidekiq_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.sidekiq_method_call_timings_per_method FROM gitlab.gitlab_30d.sidekiq_method_calls GROUP BY time(1m), method END
+CREATE CONTINUOUS QUERY rails_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.rails_memory_usage_per_minute FROM gitlab.gitlab_30d.rails_memory_usage GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.sidekiq_memory_usage_per_minute FROM gitlab.gitlab_30d.sidekiq_memory_usage GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.sidekiq_file_descriptors_per_minute FROM gitlab.gitlab_30d.sidekiq_file_descriptors GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.rails_file_descriptors_per_minute FROM gitlab.gitlab_30d.rails_file_descriptors GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.rails_gc_counts_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.sidekiq_gc_counts_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.rails_gc_timings_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.sidekiq_gc_timings_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.rails_gc_major_minor_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.sidekiq_gc_major_minor_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END
+```
+
+## Import Dashboards
+
+You can now import a set of default dashboards that will give you a good
+start on displaying useful information. GitLab has published a set of default
+[Grafana dashboards][grafana-dashboards] to get you started. Clone the
+repository or download a zip/tarball, then follow these steps to import each
+JSON file.
+
+Open the dashboard dropdown menu and click 'Import'
+
+![Grafana dashboard dropdown](/img/grafana_dashboard_dropdown.png)
+
+Click 'Choose file' and browse to the location where you downloaded or cloned
+the dashboard repository. Pick one of the JSON files to import.
+
+![Grafana dashboard import](/img/grafana_dashboard_import.png)
+
+Once the dashboard is imported, be sure to click save icon in the top bar. If
+you do not save the dashboard after importing it will be removed when you
+navigate away.
+
+![Grafana save icon](/img/grafana_save_icon.png)
+
+Repeat this process for each dashboard you wish to import.
+
+[grafana-dashboards]: https://gitlab.com/gitlab-org/grafana-dashboards
+
+---
+
+Read more on:
+
+- [Introduction to GitLab Performance Monitoring](introduction.md)
+- [GitLab Configuration](gitlab_configuration.md)
+- [InfluxDB Installation/Configuration](influxdb_configuration.md)
+- [InfluxDB Schema](influxdb_schema.md)
diff --git a/doc/monitoring/performance/img/grafana_dashboard_dropdown.png b/doc/monitoring/performance/img/grafana_dashboard_dropdown.png
new file mode 100644
index 00000000000..b4448c7a09f
--- /dev/null
+++ b/doc/monitoring/performance/img/grafana_dashboard_dropdown.png
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_dashboard_import.png b/doc/monitoring/performance/img/grafana_dashboard_import.png
new file mode 100644
index 00000000000..5a2d3c0937a
--- /dev/null
+++ b/doc/monitoring/performance/img/grafana_dashboard_import.png
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_data_source_configuration.png b/doc/monitoring/performance/img/grafana_data_source_configuration.png
new file mode 100644
index 00000000000..7e2e111f570
--- /dev/null
+++ b/doc/monitoring/performance/img/grafana_data_source_configuration.png
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_data_source_empty.png b/doc/monitoring/performance/img/grafana_data_source_empty.png
new file mode 100644
index 00000000000..11e27571e64
--- /dev/null
+++ b/doc/monitoring/performance/img/grafana_data_source_empty.png
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_save_icon.png b/doc/monitoring/performance/img/grafana_save_icon.png
new file mode 100644
index 00000000000..3d4265bee8e
--- /dev/null
+++ b/doc/monitoring/performance/img/grafana_save_icon.png
Binary files differ
diff --git a/doc/monitoring/performance/introduction.md b/doc/monitoring/performance/introduction.md
index f2460d31302..79904916b7e 100644
--- a/doc/monitoring/performance/introduction.md
+++ b/doc/monitoring/performance/introduction.md
@@ -8,8 +8,9 @@ Apart from this introduction, you are advised to read through the following
documents in order to understand and properly configure GitLab Performance Monitoring:
- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Configuration](influxdb_configuration.md)
+- [InfluxDB Install/Configuration](influxdb_configuration.md)
- [InfluxDB Schema](influxdb_schema.md)
+- [Grafana Install/Configuration](grafana_configuration.md)
## Introduction to GitLab Performance Monitoring
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index f6d1234ac4a..4329ac30a1c 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -249,6 +249,9 @@ reconfigure` after changing `gitlab-secrets.json`.
### Installation from source
```
+# Stop processes that are connected to the database
+sudo service gitlab stop
+
bundle exec rake gitlab:backup:restore RAILS_ENV=production
```
diff --git a/doc/update/README.md b/doc/update/README.md
index 109d5de3fa2..0241f036830 100644
--- a/doc/update/README.md
+++ b/doc/update/README.md
@@ -15,3 +15,4 @@ Depending on the installation method and your GitLab version, there are multiple
- [MySQL to PostgreSQL](mysql_to_postgresql.md) guides you through migrating your database from MySQL to PostgreSQL.
- [MySQL installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/database_mysql.md) contains additional information about configuring GitLab to work with a MySQL database.
+- [Restoring from backup after a failed upgrade](restore_after_failure.md)
diff --git a/doc/update/restore_after_failure.md b/doc/update/restore_after_failure.md
new file mode 100644
index 00000000000..01c52aae7f5
--- /dev/null
+++ b/doc/update/restore_after_failure.md
@@ -0,0 +1,83 @@
+# Restoring from backup after a failed upgrade
+
+Upgrades are usually smooth and restoring from backup is a rare occurrence.
+However, it's important to know how to recover when problems do arise.
+
+## Roll back to an earlier version and restore a backup
+
+In some cases after a failed upgrade, the fastest solution is to roll back to
+the previous version you were using.
+
+First, roll back the code or package. For source installations this involves
+checking out the older version (branch or tag). For Omnibus installations this
+means installing the older .deb or .rpm package. Then, restore from a backup.
+Follow the instructions in the
+[Backup and Restore](../raketasks/backup_restore.md#restore-a-previously-created-backup)
+documentation.
+
+## Potential problems on the next upgrade
+
+When a rollback is necessary it can produce problems on subsequent upgrade
+attempts. This is because some tables may have been added during the failed
+upgrade. If these tables are still present after you restore from the
+older backup it can lead to migration failures on future upgrades.
+
+Starting in GitLab 8.6 we drop all tables prior to importing the backup to
+prevent this problem. If you've restored a backup to a version prior to 8.6 you
+may need to manually correct the problem next time you upgrade.
+
+Example error:
+
+```
+== 20151103134857 CreateLfsObjects: migrating =================================
+-- create_table(:lfs_objects)
+rake aborted!
+StandardError: An error has occurred, this and all later migrations canceled:
+
+PG::DuplicateTable: ERROR: relation "lfs_objects" already exists
+```
+
+Copy the version from the error. In this case the version number is
+`20151103134857`.
+
+>**WARNING:** Use the following steps only if you are certain this is what you
+need to do.
+
+### GitLab 8.6+
+
+Pass the version to a database rake task to manually mark the migration as
+complete.
+
+```
+# Source install
+sudo -u git -H bundle exec rake gitlab:db:mark_migration_complete[20151103134857] RAILS_ENV=production
+
+# Omnibus install
+sudo gitlab-rake gitlab:db:mark_migration_complete[20151103134857]
+```
+
+Once the migration is successfully marked, run the rake `db:migrate` task again.
+You will likely have to repeat this process several times until all failed
+migrations are marked complete.
+
+### GitLab < 8.6
+
+```
+# Source install
+sudo -u git -H bundle exec rails console production
+
+# Omnibus install
+sudo gitlab-rails console
+```
+
+At the Rails console, type the following commands:
+
+```
+ActiveRecord::Base.connection.execute("INSERT INTO schema_migrations (version) VALUES('20151103134857')")
+exit
+```
+
+Once the migration is successfully marked, run the rake `db:migrate` task again.
+You will likely have to repeat this process several times until all failed
+migrations are marked complete.
+
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 2228425076b..b7209c14148 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -242,9 +242,9 @@ module Ci
stage_index = stages.index(job[:stage])
job[:dependencies].each do |dependency|
- raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency]
+ raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym]
- unless stages.index(@jobs[dependency][:stage]) < stage_index
+ unless stages.index(@jobs[dependency.to_sym][:stage]) < stage_index
raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
end
end
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index 2ca21af5bc8..d4b6f6d120d 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -45,12 +45,12 @@ module Gitlab
note = create_note(reply)
unless note.persisted?
- message = "The comment could not be created for the following reasons:"
+ msg = "The comment could not be created for the following reasons:"
note.errors.full_messages.each do |error|
- message << "\n\n- #{error}"
+ msg << "\n\n- #{error}"
end
- raise InvalidNoteError, message
+ raise InvalidNoteError, msg
end
end
@@ -63,13 +63,13 @@ module Gitlab
end
def reply_key
- reply_key = nil
+ key = nil
message.to.each do |address|
- reply_key = Gitlab::IncomingEmail.key_from_address(address)
- break if reply_key
+ key = Gitlab::IncomingEmail.key_from_address(address)
+ break if key
end
- reply_key
+ key
end
def sent_notification
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index cb4abe13799..402bb338f27 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -22,7 +22,7 @@ namespace :gitlab do
end
# Restore backup of GitLab system
- desc "GitLab | Restore a previously created backup"
+ desc 'GitLab | Restore a previously created backup'
task restore: :environment do
warn_user_is_not_gitlab
configure_cron_mode
@@ -30,13 +30,31 @@ namespace :gitlab do
backup = Backup::Manager.new
backup.unpack
- Rake::Task["gitlab:backup:db:restore"].invoke unless backup.skipped?("db")
- Rake::Task["gitlab:backup:repo:restore"].invoke unless backup.skipped?("repositories")
- Rake::Task["gitlab:backup:uploads:restore"].invoke unless backup.skipped?("uploads")
- Rake::Task["gitlab:backup:builds:restore"].invoke unless backup.skipped?("builds")
- Rake::Task["gitlab:backup:artifacts:restore"].invoke unless backup.skipped?("artifacts")
- Rake::Task["gitlab:backup:lfs:restore"].invoke unless backup.skipped?("lfs")
- Rake::Task["gitlab:shell:setup"].invoke
+ unless backup.skipped?('db')
+ unless ENV['force'] == 'yes'
+ warning = warning = <<-MSG.strip_heredoc
+ Before restoring the database we recommend removing all existing
+ tables to avoid future upgrade problems. Be aware that if you have
+ custom tables in the GitLab database these tables and all data will be
+ removed.
+ MSG
+ ask_to_continue
+ puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.yellow
+ sleep(5)
+ end
+ # Drop all tables Load the schema to ensure we don't have any newer tables
+ # hanging out from a failed upgrade
+ $progress.puts 'Cleaning the database ... '.blue
+ Rake::Task['gitlab:db:drop_tables'].invoke
+ $progress.puts 'done'.green
+ Rake::Task['gitlab:backup:db:restore'].invoke
+ end
+ Rake::Task['gitlab:backup:repo:restore'].invoke unless backup.skipped?('repositories')
+ Rake::Task['gitlab:backup:uploads:restore'].invoke unless backup.skipped?('uploads')
+ Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds')
+ Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts')
+ Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs')
+ Rake::Task['gitlab:shell:setup'].invoke
backup.cleanup
end
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
new file mode 100644
index 00000000000..4921c6e0bcf
--- /dev/null
+++ b/lib/tasks/gitlab/db.rake
@@ -0,0 +1,35 @@
+namespace :gitlab do
+ namespace :db do
+ desc 'GitLab | Manually insert schema migration version'
+ task :mark_migration_complete, [:version] => :environment do |_, args|
+ unless args[:version]
+ puts "Must specify a migration version as an argument".red
+ exit 1
+ end
+
+ version = args[:version].to_i
+ if version == 0
+ puts "Version '#{args[:version]}' must be a non-zero integer".red
+ exit 1
+ end
+
+ sql = "INSERT INTO schema_migrations (version) VALUES (#{version})"
+ begin
+ ActiveRecord::Base.connection.execute(sql)
+ puts "Successfully marked '#{version}' as complete".green
+ rescue ActiveRecord::RecordNotUnique
+ puts "Migration version '#{version}' is already marked complete".yellow
+ end
+ end
+
+ desc 'Drop all tables'
+ task :drop_tables => :environment do
+ connection = ActiveRecord::Base.connection
+ tables = connection.tables
+ tables.delete 'schema_migrations'
+ # Truncate schema_migrations to ensure migrations re-run
+ connection.execute('TRUNCATE schema_migrations')
+ tables.each { |t| connection.execute("DROP TABLE #{t}") }
+ end
+ end
+end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 938e97298b6..465531b2b36 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -1,10 +1,15 @@
require 'rails_helper'
describe GroupsController do
- describe 'GET index' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+ let!(:group_member) { create(:group_member, group: group, user: user) }
+
+ describe 'GET #index' do
context 'as a user' do
it 'redirects to Groups Dashboard' do
- sign_in(create(:user))
+ sign_in(user)
get :index
@@ -20,4 +25,54 @@ describe GroupsController do
end
end
end
+
+ describe 'GET #issues' do
+ let(:issue_1) { create(:issue, project: project) }
+ let(:issue_2) { create(:issue, project: project) }
+
+ before do
+ create_list(:upvote_note, 3, project: project, noteable: issue_2)
+ create_list(:upvote_note, 2, project: project, noteable: issue_1)
+ create_list(:downvote_note, 2, project: project, noteable: issue_2)
+
+ sign_in(user)
+ end
+
+ context 'sorting by votes' do
+ it 'sorts most popular issues' do
+ get :issues, id: group.to_param, sort: 'upvotes_desc'
+ expect(assigns(:issues)).to eq [issue_2, issue_1]
+ end
+
+ it 'sorts least popular issues' do
+ get :issues, id: group.to_param, sort: 'downvotes_desc'
+ expect(assigns(:issues)).to eq [issue_2, issue_1]
+ end
+ end
+ end
+
+ describe 'GET #merge_requests' do
+ let(:merge_request_1) { create(:merge_request, source_project: project) }
+ let(:merge_request_2) { create(:merge_request, :simple, source_project: project) }
+
+ before do
+ create_list(:upvote_note, 3, project: project, noteable: merge_request_2)
+ create_list(:upvote_note, 2, project: project, noteable: merge_request_1)
+ create_list(:downvote_note, 2, project: project, noteable: merge_request_2)
+
+ sign_in(user)
+ end
+
+ context 'sorting by votes' do
+ it 'sorts most popular merge requests' do
+ get :merge_requests, id: group.to_param, sort: 'upvotes_desc'
+ expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1]
+ end
+
+ it 'sorts least popular merge requests' do
+ get :merge_requests, id: group.to_param, sort: 'downvotes_desc'
+ expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1]
+ end
+ end
+ end
end
diff --git a/spec/features/dashboard_milestones_spec.rb b/spec/features/dashboard_milestones_spec.rb
new file mode 100644
index 00000000000..f32fddbc9fa
--- /dev/null
+++ b/spec/features/dashboard_milestones_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+feature 'Dashboard > Milestones', feature: true do
+ describe 'as anonymous user' do
+ before do
+ visit dashboard_milestones_path
+ end
+
+ it 'is redirected to sign-in page' do
+ expect(current_path).to eq new_user_session_path
+ end
+ end
+
+ describe 'as logged-in user' do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
+ let!(:milestone) { create(:milestone, project: project) }
+ before do
+ project.team << [user, :master]
+ login_with(user)
+ visit dashboard_milestones_path
+ end
+
+ it 'sees milestones' do
+ expect(current_path).to eq dashboard_milestones_path
+ expect(page).to have_content(milestone.title)
+ end
+ end
+end
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
new file mode 100644
index 00000000000..121954fabca
--- /dev/null
+++ b/spec/features/issues/update_issues_spec.rb
@@ -0,0 +1,117 @@
+require 'rails_helper'
+
+feature 'Multiple issue updating from issues#index', feature: true do
+ let!(:project) { create(:project) }
+ let!(:issue) { create(:issue, project: project) }
+ let!(:user) { create(:user)}
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'status', js: true do
+ it 'should be set to closed' do
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('#check_all_issues').click
+ find('.js-issue-status').click
+
+ find('.dropdown-menu-status a', text: 'Closed').click
+ click_update_issues_button
+ expect(page).to have_selector('.issue', count: 0)
+ end
+
+ it 'should be set to open' do
+ create_closed
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('.issues-state-filters a', text: 'Closed').click
+
+ find('#check_all_issues').click
+ find('.js-issue-status').click
+
+ find('.dropdown-menu-status a', text: 'Open').click
+ click_update_issues_button
+ expect(page).to have_selector('.issue', count: 0)
+ end
+ end
+
+ context 'assignee', js: true do
+ it 'should update to current user' do
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('#check_all_issues').click
+ find('.js-update-assignee').click
+
+ find('.dropdown-menu-user-link', text: user.username).click
+ click_update_issues_button
+
+ page.within('.issue .controls') do
+ expect(find('.author_link')["data-original-title"]).to have_content(user.name)
+ end
+ end
+
+ it 'should update to unassigned' do
+ create_assigned
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('#check_all_issues').click
+ find('.js-update-assignee').click
+
+ find('.dropdown-menu-user-link', text: "Unassigned").click
+ click_update_issues_button
+
+ within first('.issue .controls') do
+ expect(page).to have_no_selector('.author_link')
+ end
+ end
+ end
+
+ context 'milestone', js: true do
+ let(:milestone) { create(:milestone, project: project) }
+
+ it 'should update milestone' do
+ visit namespace_project_issues_path(project.namespace, project)
+
+ find('#check_all_issues').click
+ find('.issues_bulk_update .js-milestone-select').click
+
+ find('.dropdown-menu-milestone a', text: milestone.title).click
+ click_update_issues_button
+
+ expect(find('.issue')).to have_content milestone.title
+ end
+
+ it 'should set to no milestone' do
+ create_with_milestone
+ visit namespace_project_issues_path(project.namespace, project)
+
+ expect(first('.issue')).to have_content milestone.title
+
+ find('#check_all_issues').click
+ find('.issues_bulk_update .js-milestone-select').click
+
+ find('.dropdown-menu-milestone a', text: "No Milestone").click
+ click_update_issues_button
+
+ expect(first('.issue')).to_not have_content milestone.title
+ end
+ end
+
+ def create_closed
+ create(:issue, project: project, state: :closed)
+ end
+
+ def create_assigned
+ create(:issue, project: project, assignee: user)
+ end
+
+ def create_with_milestone
+ create(:issue, project: project, milestone: milestone)
+ end
+
+ def click_update_issues_button
+ find('.update_selected_issues').click
+ end
+end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index b79b8147ce0..dcb8a3451bd 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -492,19 +492,25 @@ module Ci
end
context 'dependencies to builds' do
+ let(:dependencies) { ['build1', 'build2'] }
+
+ it { expect { subject }.to_not raise_error }
+ end
+
+ context 'dependencies to builds defined as symbols' do
let(:dependencies) { [:build1, :build2] }
it { expect { subject }.to_not raise_error }
end
context 'undefined dependency' do
- let(:dependencies) { [:undefined] }
+ let(:dependencies) { ['undefined'] }
it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') }
end
context 'dependencies to deploy' do
- let(:dependencies) { [:deploy] }
+ let(:dependencies) { ['deploy'] }
it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') }
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 3c34b1d397f..15052aaca28 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -152,6 +152,11 @@ describe Issue, models: true do
it { is_expected.to eq true }
+ context 'issue not persisted' do
+ let(:issue) { build(:issue, project: project) }
+ it { is_expected.to eq false }
+ end
+
context 'checking destination project also' do
subject { issue.can_move?(user, to_project) }
let(:to_project) { create(:project) }
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index 14cc20e529a..9b0c73aaf37 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -85,6 +85,10 @@ describe Issues::MoveService, services: true do
expect(old_issue.moved?).to eq true
expect(old_issue.moved_to).to eq new_issue
end
+
+ it 'preserves create time' do
+ expect(old_issue.created_at).to eq new_issue.created_at
+ end
end
context 'issue with notes' do
@@ -121,10 +125,23 @@ describe Issues::MoveService, services: true do
it 'preserves orignal author of comment' do
expect(user_notes.pluck(:author_id)).to all(eq(author.id))
end
+ end
+
+ context 'note that has been updated' do
+ let!(:note) do
+ create(:note, noteable: old_issue, project: old_project,
+ author: author, updated_at: Date.yesterday,
+ created_at: Date.yesterday)
+ end
+
+ include_context 'issue move executed'
it 'preserves time when note has been created at' do
- expect(old_issue.notes.first.created_at)
- .to eq new_issue.notes.first.created_at
+ expect(new_issue.notes.first.created_at).to eq note.created_at
+ end
+
+ it 'preserves time when note has been updated at' do
+ expect(new_issue.notes.first.updated_at).to eq note.updated_at
end
end
@@ -208,6 +225,12 @@ describe Issues::MoveService, services: true do
it { expect { move }.to raise_error(StandardError, /permissions/) }
end
+
+ context 'issue is not persisted' do
+ include_context 'user can move issue'
+ let(:old_issue) { build(:issue, project: old_project, author: author) }
+ it { expect { move }.to raise_error(StandardError, /permissions/) }
+ end
end
end
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 4ffe753fef5..6b214a0d96b 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -151,7 +151,12 @@ describe Issues::UpdateService, services: true do
context 'when the issue is relabeled' do
let!(:non_subscriber) { create(:user) }
- let!(:subscriber) { create(:user).tap { |u| label.toggle_subscription(u) } }
+ let!(:subscriber) do
+ create(:user).tap do |u|
+ label.toggle_subscription(u)
+ project.team << [u, :developer]
+ end
+ end
it 'sends notifications for subscribers of newly added labels' do
opts = { label_ids: [label.id] }
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index b5407397c1d..0f2aa3ae73c 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -111,6 +111,33 @@ describe NotificationService, services: true do
end
end
+ context 'confidential issue note' do
+ let(:project) { create(:empty_project, :public) }
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+ let(:note) { create(:note_on_issue, noteable: confidential_issue, project: project, note: "#{author.to_reference} #{assignee.to_reference} #{non_member.to_reference} #{member.to_reference} #{admin.to_reference}") }
+
+ it 'filters out users that can not read the issue' do
+ project.team << [member, :developer]
+
+ expect(SentNotification).to receive(:record).with(confidential_issue, any_args).exactly(4).times
+
+ ActionMailer::Base.deliveries.clear
+
+ notification.new_note(note)
+
+ should_not_email(non_member)
+ should_email(author)
+ should_email(assignee)
+ should_email(member)
+ should_email(admin)
+ end
+ end
+
context 'issue note mention' do
let(:project) { create(:empty_project, :public) }
let(:issue) { create(:issue, project: project, assignee: create(:user)) }
@@ -233,6 +260,36 @@ describe NotificationService, services: true do
should_email(subscriber)
end
+
+ context 'confidential issues' do
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
+
+ it "emails subscribers of the issue's labels that can read the issue" do
+ project.team << [member, :developer]
+
+ label = create(:label, issues: [confidential_issue])
+ label.toggle_subscription(non_member)
+ label.toggle_subscription(author)
+ label.toggle_subscription(assignee)
+ label.toggle_subscription(member)
+ label.toggle_subscription(admin)
+
+ ActionMailer::Base.deliveries.clear
+
+ notification.new_issue(confidential_issue, @u_disabled)
+
+ should_not_email(non_member)
+ should_not_email(author)
+ should_email(assignee)
+ should_email(member)
+ should_email(admin)
+ end
+ end
end
describe :reassigned_issue do
@@ -332,6 +389,37 @@ describe NotificationService, services: true do
should_not_email(subscriber_to_label)
should_email(subscriber_to_label2)
end
+
+ context 'confidential issues' do
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
+ let!(:label_1) { create(:label, issues: [confidential_issue]) }
+ let!(:label_2) { create(:label) }
+
+ it "emails subscribers of the issue's labels that can read the issue" do
+ project.team << [member, :developer]
+
+ label_2.toggle_subscription(non_member)
+ label_2.toggle_subscription(author)
+ label_2.toggle_subscription(assignee)
+ label_2.toggle_subscription(member)
+ label_2.toggle_subscription(admin)
+
+ ActionMailer::Base.deliveries.clear
+
+ notification.relabeled_issue(confidential_issue, [label_2], @u_disabled)
+
+ should_not_email(non_member)
+ should_email(author)
+ should_email(assignee)
+ should_email(member)
+ should_email(admin)
+ end
+ end
end
describe :close_issue do
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 63bed2414df..320be9a0b61 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -3,9 +3,10 @@ require 'rake'
describe 'gitlab:app namespace rake task' do
before :all do
- Rake.application.rake_require "tasks/gitlab/task_helpers"
- Rake.application.rake_require "tasks/gitlab/backup"
- Rake.application.rake_require "tasks/gitlab/shell"
+ Rake.application.rake_require 'tasks/gitlab/task_helpers'
+ Rake.application.rake_require 'tasks/gitlab/backup'
+ Rake.application.rake_require 'tasks/gitlab/shell'
+ Rake.application.rake_require 'tasks/gitlab/db'
# empty task as env is already loaded
Rake::Task.define_task :environment
end
@@ -37,6 +38,7 @@ describe 'gitlab:app namespace rake task' do
allow(FileUtils).to receive(:mv).and_return(true)
allow(Rake::Task["gitlab:shell:setup"]).
to receive(:invoke).and_return(true)
+ ENV['force'] = 'yes'
end
let(:gitlab_version) { Gitlab::VERSION }
@@ -52,13 +54,14 @@ describe 'gitlab:app namespace rake task' do
it 'should invoke restoration on match' do
allow(YAML).to receive(:load_file).
and_return({ gitlab_version: gitlab_version })
- expect(Rake::Task["gitlab:backup:db:restore"]).to receive(:invoke)
- expect(Rake::Task["gitlab:backup:repo:restore"]).to receive(:invoke)
- expect(Rake::Task["gitlab:backup:builds:restore"]).to receive(:invoke)
- expect(Rake::Task["gitlab:backup:uploads:restore"]).to receive(:invoke)
- expect(Rake::Task["gitlab:backup:artifacts:restore"]).to receive(:invoke)
- expect(Rake::Task["gitlab:backup:lfs:restore"]).to receive(:invoke)
- expect(Rake::Task["gitlab:shell:setup"]).to receive(:invoke)
+ expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:db:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:repo:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:builds:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:uploads:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end
end
@@ -177,17 +180,18 @@ describe 'gitlab:app namespace rake task' do
end
it 'does not invoke repositories restore' do
- allow(Rake::Task["gitlab:shell:setup"]).
+ allow(Rake::Task['gitlab:shell:setup']).
to receive(:invoke).and_return(true)
allow($stdout).to receive :write
- expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke
- expect(Rake::Task["gitlab:backup:repo:restore"]).not_to receive :invoke
- expect(Rake::Task["gitlab:backup:uploads:restore"]).not_to receive :invoke
- expect(Rake::Task["gitlab:backup:builds:restore"]).to receive :invoke
- expect(Rake::Task["gitlab:backup:artifacts:restore"]).to receive :invoke
- expect(Rake::Task["gitlab:backup:lfs:restore"]).to receive :invoke
- expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke
+ expect(Rake::Task['gitlab:db:drop_tables']).to receive :invoke
+ expect(Rake::Task['gitlab:backup:db:restore']).to receive :invoke
+ expect(Rake::Task['gitlab:backup:repo:restore']).not_to receive :invoke
+ expect(Rake::Task['gitlab:backup:uploads:restore']).not_to receive :invoke
+ expect(Rake::Task['gitlab:backup:builds:restore']).to receive :invoke
+ expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive :invoke
+ expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke
+ expect(Rake::Task['gitlab:shell:setup']).to receive :invoke
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end
end