diff options
153 files changed, 1287 insertions, 562 deletions
diff --git a/.eslintignore b/.eslintignore index d9c2233c9d7..93de4b10dfe 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ +/coverage/ /coverage-javascript/ /public/ /tmp/ diff --git a/.eslintrc b/.eslintrc index 788a88487d8..b80dcec9d1d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,15 +1,14 @@ { "env": { + "jquery": true, "browser": true, "es6": true }, - "extends": "airbnb", + "extends": "airbnb-base", "globals": { - "$": false, "_": false, "gl": false, - "gon": false, - "jQuery": false + "gon": false }, "plugins": [ "filenames" diff --git a/CHANGELOG.md b/CHANGELOG.md index 549336e4dff..12a3e63ed2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.14.1 (2016-11-28) + +- Fix deselecting calendar days on contribution graph. !6453 (ClemMakesApps) +- Update grape entity to 0.6.0. !7491 +- If Build running change accept merge request when build succeeds button from orange to blue. !7577 +- Changed import sources buttons to checkboxes. !7598 (Luke "Jared" Bennett) +- Last minute CI Style tweaks for 8.14. !7643 +- Fix exceptions when loading build trace. !7658 +- Fix wrong template rendered when CI/CD settings aren't update successfully. !7665 +- fixes last_deployment call environment is nil. !7671 +- Sort builds by name within pipeline graph. !7681 +- Correctly determine mergeability of MR with no discussions. +- Sidekiq stats in the admin area will now show correctly on different platforms. (blackst0ne) +- Fixed issue boards dragging card removing random issues. +- Fix information disclosure in `Projects::BlobController#update`. +- Fix missing access checks on issue lookup using IssuableFinder. +- Replace issue access checks with use of IssuableFinder. +- Non members cannot create labels through the API. +- Fix cycle analytics plan stage when commits are missing. + ## 8.14.0 (2016-11-22) - Use separate email-token for incoming email and revert back the inactive feature. !5914 @@ -202,6 +222,15 @@ entry. - Fix "Without projects" filter. !6611 (Ben Bodenmiller) - Fix 404 when visit /projects page +## 8.13.7 (2016-11-28) + +- fixes 500 error on project show when user is not logged in and project is still empty. !7376 +- Update grape entity to 0.6.0. !7491 +- Fix information disclosure in `Projects::BlobController#update`. +- Fix missing access checks on issue lookup using IssuableFinder. +- Replace issue access checks with use of IssuableFinder. +- Non members cannot create labels through the API. + ## 8.13.6 (2016-11-17) - Omniauth auto link LDAP user falls back to find by DN when user cannot be found by UID. !7002 diff --git a/README.md b/README.md index f63543ca39d..c9bb5af6da2 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ For more information please see the [architecture documentation](https://docs.gi ## UX design -Please adhere to the [UX Guide](doc/development/ux_guide/readme.md) when creating designs and implementing code. +Please adhere to the [UX Guide](doc/development/ux_guide/index.md) when creating designs and implementing code. ## Third-party applications diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index c2d4670b7e9..16df4b0b005 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -208,6 +208,9 @@ new gl.ProtectedBranchCreate(); new gl.ProtectedBranchEditList(); break; + case 'projects:variables:index': + new gl.ProjectVariables(); + break; } switch (path.first()) { case 'admin': diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 35e183a9086..84faabf938a 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -181,7 +181,7 @@ <div class="environments-container"> <div class="environments-list-loading text-center" v-if="isLoading"> - <i class="fa fa-spinner spin"></i> + <i class="fa fa-spinner fa-spin"></i> </div> <div class="blank-state blank-state-no-icon" diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 812d5cde685..f334f35594d 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, no-undef, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks, max-len */ +/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, no-undef, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks, max-len */ (function() { this.LabelsSelect = (function() { function LabelsSelect() { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 47e7b6f831b..0ca0e255595 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, no-undef, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks, max-len */ +/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, no-undef, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks, max-len */ /*= require autosave */ /*= require autosize */ diff --git a/app/assets/javascripts/project_variables.js.es6 b/app/assets/javascripts/project_variables.js.es6 new file mode 100644 index 00000000000..4ee2e49306d --- /dev/null +++ b/app/assets/javascripts/project_variables.js.es6 @@ -0,0 +1,43 @@ +(() => { + const HIDDEN_VALUE_TEXT = '******'; + + class ProjectVariables { + constructor() { + this.$revealBtn = $('.js-btn-toggle-reveal-values'); + this.$revealBtn.on('click', this.toggleRevealState.bind(this)); + } + + toggleRevealState(e) { + e.preventDefault(); + + const oldStatus = this.$revealBtn.attr('data-status'); + let newStatus = 'hidden'; + let newAction = 'Reveal Values'; + + if (oldStatus === 'hidden') { + newStatus = 'revealed'; + newAction = 'Hide Values'; + } + + this.$revealBtn.attr('data-status', newStatus); + + const $variables = $('.variable-value'); + + $variables.each((_, variable) => { + const $variable = $(variable); + let newText = HIDDEN_VALUE_TEXT; + + if (newStatus === 'revealed') { + newText = $variable.attr('data-value'); + } + + $variable.text(newText); + }); + + this.$revealBtn.text(newAction); + } + } + + window.gl = window.gl || {}; + window.gl.ProjectVariables = ProjectVariables; +})(); diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index ad0d387067f..c0dd1cb3667 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -80,6 +80,7 @@ border-radius: 0; border: none; height: auto; + width: 100%; margin: 0; align-self: center; } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 4a9aa0f8717..36f530af685 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -15,7 +15,7 @@ @include btn-default; } -@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border) { +@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border, $active-background, $active-border) { background-color: $background; color: $text; border-color: $border; @@ -23,8 +23,14 @@ &:hover, &:focus { background-color: $hover-background; - color: $hover-text; border-color: $hover-border; + color: $hover-text; + } + + &:active { + background-color: $active-background; + border-color: $active-border; + color: $hover-text; } } @@ -82,11 +88,11 @@ } @mixin btn-gray { - @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-light, $gray-dark, $border-gray-dark, $gl-gray-dark); + @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-gray-dark); } @mixin btn-white { - @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $btn-white-active); + @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $gl-text-color); } @mixin btn-with-margin { @@ -139,11 +145,11 @@ &.btn-new, &.btn-create, &.btn-save { - @include btn-outline($white-light, $green-normal, $green-normal, $green-light, $white-light, $green-light); + @include btn-outline($white-light, $border-green-light, $border-green-light, $green-light, $white-light, $border-green-light, $green-normal, $border-green-normal); } &.btn-remove { - @include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light); + @include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal); } } @@ -165,11 +171,11 @@ } &.btn-close { - @include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light); + @include btn-outline($white-light, $border-orange-light, $border-orange-light, $orange-light, $white-light, $border-orange-light, $orange-normal, $border-orange-normal); } &.btn-spam { - @include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light); + @include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal); } &.btn-danger, @@ -199,7 +205,7 @@ } .fa-caret-down, - .fa-caret-up { + .fa-chevron-down { margin-left: 5px; } @@ -351,7 +357,7 @@ .btn-inverted { &-secondary { - @include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light); + @include btn-outline($white-light, $border-blue-light, $border-blue-light, $blue-light, $white-light, $border-blue-light, $blue-normal, $border-blue-normal); } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 583c17e4a83..6d77aadd753 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -8,6 +8,12 @@ } } +@mixin chevron-active { + .fa-chevron-down { + color: $dropdown-toggle-hover-icon-color; + } +} + .open { .dropdown-menu, .dropdown-menu-nav { @@ -19,53 +25,27 @@ } } + .dropdown-toggle, .dropdown-menu-toggle { + @include chevron-active; border-color: $dropdown-toggle-hover-border-color; - - .fa { - color: $dropdown-toggle-hover-icon-color; - } } } -.dropdown-menu-toggle { - position: relative; - width: 160px; - padding: 6px 20px 6px 10px; +.dropdown-toggle { + padding: 6px 8px 6px 10px; background-color: $dropdown-toggle-bg; color: $dropdown-toggle-color; font-size: 15px; text-align: left; border: 1px solid $border-color; border-radius: $border-radius-base; - text-overflow: ellipsis; white-space: nowrap; - overflow: hidden; - - .fa { - position: absolute; - top: 10px; - right: 8px; - color: $dropdown-toggle-icon-color; - - &.fa-spinner { - font-size: 16px; - margin-top: -8px; - } - } &.no-outline { outline: 0; } - &:hover, { - border-color: $dropdown-toggle-hover-border-color; - - .fa { - color: $dropdown-toggle-hover-icon-color; - } - } - &.large { width: 200px; } @@ -86,6 +66,51 @@ max-width: 100%; padding-right: 25px; } + + .fa { + color: $dropdown-toggle-icon-color; + } + + .fa-chevron-down { + font-size: $dropdown-chevron-size; + position: relative; + top: -3px; + margin-left: 5px; + } + + &:hover { + @include chevron-active; + border-color: $dropdown-toggle-hover-border-color; + } + + &:focus:active { + @include chevron-active; + border-color: $dropdown-toggle-active-border-color; + } +} + +.dropdown-menu-toggle { + @extend .dropdown-toggle; + padding-right: 20px; + position: relative; + width: 160px; + text-overflow: ellipsis; + overflow: hidden; + + .fa { + position: absolute; + + &.fa-spinner { + font-size: 16px; + margin-top: -8px; + } + } + + .fa-chevron-down { + position: absolute; + top: 11px; + right: 8px; + } } .dropdown-menu, diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index ce864c2de5e..1839ffa0976 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -51,19 +51,26 @@ margin-bottom: -1px; font-size: 15px; line-height: 28px; - color: #959494; + color: $note-toolbar-color; border-bottom: 2px solid transparent; &:hover, &:active, &:focus { text-decoration: none; + border-bottom: 2px solid $gray-darkest; + color: $black; + + .badge { + color: $black; + } } } &.active a { border-bottom: 2px solid $link-underline-blue; color: $black; + font-weight: 600; } .badge { @@ -85,14 +92,20 @@ li { + &.active a { + border-bottom: none; + color: $link-underline-blue; + } + a { margin: 0; padding: 11px 10px 9px; - } - &.active a { - border-bottom: none; - color: $link-underline-blue; + &:hover, + &:active, + &:focus { + border-bottom: none; + } } } } @@ -310,37 +323,9 @@ height: 51px; li { - a { padding-top: 10px; } - - a, - i { - color: $layout-link-gray; - } - - &.active { - - a, - i { - color: $black; - } - - svg { - path, - polygon { - fill: $black; - } - } - } - - &:hover { - a, - i { - color: $black; - } - } } } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 750d99ebabe..8a9c279d124 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -12,67 +12,71 @@ $sidebar-breakpoint: 1024px; /* * Color schema */ +$darken-normal-factor: 7%; +$darken-dark-factor: 10%; +$darken-border-factor: 5%; + $white-light: #fff; -$white-normal: #ededed; -$white-dark: #ececec; +$white-normal: darken($white-light, $darken-normal-factor); +$white-dark: darken($white-light, $darken-dark-factor); $gray-lightest: #fdfdfd; $gray-light: #fafafa; $gray-lighter: #f9f9f9; -$gray-normal: #f5f5f5; -$gray-dark: #ededed; +$gray-normal: darken($gray-light, $darken-normal-factor); +$gray-dark: darken($gray-light, $darken-dark-factor); $gray-darker: #eee; $gray-darkest: #c9c9c9; -$green-light: #38ae67; -$green-normal: #2faa60; -$green-dark: #2ca05b; +$green-light: #3cbd70; +$green-normal: darken($green-light, $darken-normal-factor); +$green-dark: darken($green-light, $darken-dark-factor); $blue-light: #2ea8e5; -$blue-normal: #2d9fd8; -$blue-dark: #2897ce; +$blue-normal: darken($blue-light, $darken-normal-factor); +$blue-dark: darken($blue-light, $darken-dark-factor); $blue-medium-light: #3498cb; -$blue-medium: #2f8ebf; -$blue-medium-dark: #2d86b4; +$blue-medium: darken($blue-medium-light, $darken-normal-factor); +$blue-medium-dark: darken($blue-medium-light, $darken-dark-factor); $blue-light-transparent: rgba(44, 159, 216, 0.05); $orange-light: #fc8a51; -$orange-normal: #e75e40; -$orange-dark: #ce5237; +$orange-normal: darken($orange-light, $darken-normal-factor); +$orange-dark: darken($orange-light, $darken-dark-factor); $red-light: #e52c5a; -$red-normal: #d22852; -$red-dark: darken($red-normal, 5%); +$red-normal: darken($red-light, $darken-normal-factor); +$red-dark: darken($red-light, $darken-dark-factor); $black: #000; $black-transparent: rgba(0, 0, 0, 0.3); -$border-white-light: #f1f2f4; -$border-white-normal: #d6dae2; -$border-white-dark: #c6cacf; +$border-white-light: darken($white-light, $darken-border-factor); +$border-white-normal: darken($white-normal, $darken-border-factor); +$border-white-dark: darken($white-dark, $darken-border-factor); -$border-gray-light: #dcdcdc; -$border-gray-normal: #d7d7d7; -$border-gray-dark: #c6cacf; +$border-gray-light: darken($gray-light, $darken-border-factor); +$border-gray-normal: darken($gray-normal, $darken-border-factor); +$border-gray-dark: darken($gray-dark, $darken-border-factor); $border-green-extra-light: #9adb84; -$border-green-light: #2faa60; -$border-green-normal: #2ca05b; -$border-green-dark: #279654; +$border-green-light: darken($green-light, $darken-border-factor); +$border-green-normal: darken($green-normal, $darken-border-factor); +$border-green-dark: darken($green-dark, $darken-border-factor); -$border-blue-light: #2d9fd8; -$border-blue-normal: #2897ce; -$border-blue-dark: #258dc1; +$border-blue-light: darken($blue-light, $darken-border-factor); +$border-blue-normal: darken($blue-normal, $darken-border-factor); +$border-blue-dark: darken($blue-dark, $darken-border-factor); -$border-orange-light: #fc6d26; -$border-orange-normal: #ce5237; -$border-orange-dark: #c14e35; +$border-orange-light: darken($orange-light, $darken-border-factor); +$border-orange-normal: darken($orange-normal, $darken-border-factor); +$border-orange-dark: darken($orange-dark, $darken-border-factor); -$border-red-light: #d22852; -$border-red-normal: #ca264f; -$border-red-dark: darken($border-red-normal, 5%); +$border-red-light: darken($red-light, $darken-border-factor); +$border-red-normal: darken($red-normal, $darken-border-factor); +$border-red-dark: darken($red-dark, $darken-border-factor); $help-well-bg: $gray-light; $help-well-border: #e5e5e5; @@ -216,7 +220,7 @@ $dropdown-bg: #fff; $dropdown-link-color: #555; $dropdown-link-hover-bg: $row-hover; $dropdown-empty-row-bg: rgba(#000, .04); -$dropdown-border-color: rgba(#000, .1); +$dropdown-border-color: $border-color; $dropdown-shadow-color: rgba(#000, .1); $dropdown-divider-color: rgba(#000, .1); $dropdown-header-color: #959494; @@ -225,13 +229,15 @@ $dropdown-input-color: #555; $dropdown-input-focus-border: $focus-border-color; $dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4); $dropdown-loading-bg: rgba(#fff, .6); +$dropdown-chevron-size: 10px; $dropdown-toggle-bg: #fff; -$dropdown-toggle-color: #626262; -$dropdown-toggle-border-color: #eaeaea; -$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%); +$dropdown-toggle-color: #5c5c5c; +$dropdown-toggle-border-color: #e5e5e5; +$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 13%); +$dropdown-toggle-active-border-color: darken($dropdown-toggle-border-color, 14%); $dropdown-toggle-icon-color: #c4c4c4; -$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color; +$dropdown-toggle-hover-icon-color: darken($dropdown-toggle-icon-color, 7%); /* * Buttons @@ -255,7 +261,7 @@ $search-input-border-color: rgba(#4688f1, .8); $search-input-focus-shadow-color: $dropdown-input-focus-shadow; $search-input-width: 220px; $location-badge-color: #aaa; -$location-badge-bg: $gray-normal; +$location-badge-bg: $dark-background-color; $location-badge-active-bg: #4f91f8; $location-icon-color: #e7e9ed; $location-icon-active-color: #807e7e; diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss index 8d1a6020ca4..8d1a6020ca4 100644 --- a/app/assets/stylesheets/mailers/repository_push_email.scss +++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 773155fe80a..7aad99eee4e 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -132,7 +132,7 @@ display: none; } - .btn-clipboard { + .btn-clipboard:hover { color: $gl-gray; } } @@ -235,6 +235,10 @@ padding-bottom: 10px; color: #999; + &:hover { + color: $gl-gray; + } + span { display: block; margin-top: 0; @@ -244,15 +248,17 @@ display: none; } + .avatar:hover { + border-color: #999; + } + .btn-clipboard { border: none; + color: #999; &:hover { background: transparent; - } - - i { - color: #999; + color: $gl-gray; } } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 19a7a97ea0d..0562ee7b178 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -876,3 +876,11 @@ pre.light-well { pointer-events: none; } } + +.variables-table { + table-layout: fixed; + + .variable-key { + width: 30%; + } +} diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index b81842e319b..c2bb8464824 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -112,6 +112,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :koding_enabled, :koding_url, :email_author_in_body, + :html_emails_enabled, :repository_checks_enabled, :metrics_packet_size, :send_user_confirmation_email, diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb index 3717c49f272..fbf9a026b10 100644 --- a/app/controllers/concerns/toggle_award_emoji.rb +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -1,11 +1,8 @@ module ToggleAwardEmoji extend ActiveSupport::Concern - included do - before_action :authenticate_user!, only: [:toggle_award_emoji] - end - def toggle_award_emoji + authenticate_user! name = params.require(:name) if awardable.user_can_award?(current_user, name) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 56ced786311..9940263ae24 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -13,7 +13,6 @@ class Projects::BlobController < Projects::ApplicationController before_action :assign_blob_vars before_action :commit, except: [:new, :create] before_action :blob, except: [:new, :create] - before_action :from_merge_request, only: [:edit, :update] before_action :require_branch_head, only: [:edit, :update] before_action :editor_variables, except: [:show, :preview, :diff] before_action :validate_diff_params, only: :diff @@ -39,14 +38,6 @@ class Projects::BlobController < Projects::ApplicationController def update @path = params[:file_path] if params[:file_path].present? - after_edit_path = - if from_merge_request && @target_branch == @ref - diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + - "##{hexdigest(@path)}" - else - namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) - end - create_commit(Files::UpdateService, success_path: after_edit_path, failure_view: :edit, failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) @@ -124,9 +115,14 @@ class Projects::BlobController < Projects::ApplicationController render_404 end - def from_merge_request - # If blob edit was initiated from merge request page - @from_merge_request ||= MergeRequest.find_by(id: params[:from_merge_request_id]) + def after_edit_path + from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid]) + if from_merge_request && @target_branch == @ref + diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + + "##{hexdigest(@path)}" + else + namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) + end end def editor_variables diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 6b9f37983c4..89d84809e3a 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -36,7 +36,7 @@ class Projects::BranchesController < Projects::ApplicationController execute(branch_name, ref) if params[:issue_iid] - issue = @project.issues.find_by(iid: params[:issue_iid]) + issue = IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:issue_iid]) SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue end diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index fd263960b93..ac639ef015b 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -6,7 +6,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController before_action :authorize_read_cycle_analytics! def show - @cycle_analytics = ::CycleAnalytics.new(@project, from: start_date(cycle_analytics_params)) + @cycle_analytics = ::CycleAnalytics.new(@project, current_user, from: start_date(cycle_analytics_params)) stats_values, cycle_analytics_json = generate_cycle_analytics_data diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb index 5685d0f4e7c..52517381c65 100644 --- a/app/controllers/projects/todos_controller.rb +++ b/app/controllers/projects/todos_controller.rb @@ -16,13 +16,7 @@ class Projects::TodosController < Projects::ApplicationController @issuable ||= begin case params[:issuable_type] when "issue" - issue = @project.issues.find(params[:issuable_id]) - - if can?(current_user, :read_issue, issue) - issue - else - render_404 - end + IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id]) when "merge_request" @project.merge_requests.find(params[:issuable_id]) end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index a48f22cee07..9a74e36870b 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -21,7 +21,7 @@ class IssuableFinder attr_accessor :current_user, :params - def initialize(current_user, params) + def initialize(current_user, params = {}) @current_user = current_user @params = params end @@ -41,6 +41,14 @@ class IssuableFinder sort(items) end + def find(*params) + execute.find(*params) + end + + def find_by(*params) + execute.find_by(*params) + end + def group return @group if defined?(@group) diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index 0b7832e6583..a653a6d59c6 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -12,7 +12,7 @@ class NotesFinder when "commit" project.notes.for_commit_id(target_id).non_diff_notes when "issue" - project.issues.visible_to_user(current_user).find(target_id).notes.inc_author + IssuesFinder.new(current_user, project_id: project.id).find(target_id).notes.inc_author when "merge_request" project.merge_requests.find(target_id).mr_and_commit_notes.inc_author when "snippet", "project_snippet" diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index cbab1fd5967..81e0b6bb5ae 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -43,7 +43,7 @@ module DropdownsHelper default_label = data_attr[:default_label] content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}") - output << icon('caret-down') + output << icon('chevron-down') output.html_safe end end diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index 96116e916dd..0d20c9092c4 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -4,6 +4,7 @@ module Emails setup_note_mail(note_id, recipient_id) @commit = @note.noteable + @discussion = @note.to_discussion if @note.diff_note? @target_url = namespace_project_commit_url(*note_target_url_options) mail_answer_thread(@commit, @@ -24,6 +25,7 @@ module Emails setup_note_mail(note_id, recipient_id) @merge_request = @note.noteable + @discussion = @note.to_discussion if @note.diff_note? @target_url = namespace_project_merge_request_url(*note_target_url_options) mail_answer_thread(@merge_request, note_thread_options(recipient_id)) end diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb index 7fd0905ee81..9dd4d9c6f24 100644 --- a/app/models/concerns/protected_branch_access.rb +++ b/app/models/concerns/protected_branch_access.rb @@ -2,6 +2,9 @@ module ProtectedBranchAccess extend ActiveSupport::Concern included do + belongs_to :protected_branch + delegate :project, to: :protected_branch + scope :master, -> { where(access_level: Gitlab::Access::MASTER) } scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } end @@ -9,4 +12,10 @@ module ProtectedBranchAccess def humanize self.class.human_access_levels[self.access_level] end + + def check_access(user) + return true if user.is_admin? + + project.team.max_member_access(user.id) >= access_level + end end diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index cb8e088d21d..ba4ee6fcf9d 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -1,14 +1,15 @@ class CycleAnalytics STAGES = %i[issue plan code test review staging production].freeze - def initialize(project, from:) + def initialize(project, current_user, from:) @project = project + @current_user = current_user @from = from @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: from, branch: nil) end def summary - @summary ||= Summary.new(@project, from: @from) + @summary ||= Summary.new(@project, @current_user, from: @from) end def permissions(user:) diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb index b46db449bf3..82f53d17ddd 100644 --- a/app/models/cycle_analytics/summary.rb +++ b/app/models/cycle_analytics/summary.rb @@ -1,12 +1,13 @@ class CycleAnalytics class Summary - def initialize(project, from:) + def initialize(project, current_user, from:) @project = project + @current_user = current_user @from = from end def new_issues - @project.issues.created_after(@from).count + IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count end def commits diff --git a/app/models/discussion.rb b/app/models/discussion.rb index de06c13481a..75a85563235 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -25,7 +25,12 @@ class Discussion to: :last_resolved_note, allow_nil: true - delegate :blob, :highlighted_diff_lines, to: :diff_file, allow_nil: true + delegate :blob, + :highlighted_diff_lines, + :diff_lines, + + to: :diff_file, + allow_nil: true def self.for_notes(notes) notes.group_by(&:discussion_id).values.map { |notes| new(notes) } @@ -159,10 +164,11 @@ class Discussion end # Returns an array of at most 16 highlighted lines above a diff note - def truncated_diff_lines + def truncated_diff_lines(highlight: true) + lines = highlight ? highlighted_diff_lines : diff_lines prev_lines = [] - highlighted_diff_lines.each do |line| + lines.each do |line| if line.meta? prev_lines.clear else diff --git a/app/models/project.rb b/app/models/project.rb index c61e63461e0..f01cb613b85 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -687,9 +687,9 @@ class Project < ActiveRecord::Base self.id end - def get_issue(issue_id) + def get_issue(issue_id, current_user) if default_issues_tracker? - issues.find_by(iid: issue_id) + IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id) else ExternalIssue.new(issue_id, self) end diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index 806b3ccd275..771e3376613 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -1,9 +1,6 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base include ProtectedBranchAccess - belongs_to :protected_branch - delegate :project, to: :protected_branch - validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER, Gitlab::Access::DEVELOPER] } @@ -13,10 +10,4 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base Gitlab::Access::DEVELOPER => "Developers + Masters" }.with_indifferent_access end - - def check_access(user) - return true if user.is_admin? - - project.team.max_member_access(user.id) >= access_level - end end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index 92e9c51d883..14610cb42b7 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -1,9 +1,6 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base include ProtectedBranchAccess - belongs_to :protected_branch - delegate :project, to: :protected_branch - validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS] } @@ -18,8 +15,7 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base def check_access(user) return false if access_level == Gitlab::Access::NO_ACCESS - return true if user.is_admin? - project.team.max_member_access(user.id) >= access_level + super end end diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb index cf1c418a88e..b5384e6462b 100644 --- a/app/serializers/build_entity.rb +++ b/app/serializers/build_entity.rb @@ -16,6 +16,9 @@ class BuildEntity < Grape::Entity path_to(:play_namespace_project_build, build) end + expose :created_at + expose :updated_at + private def path_to(route, build) diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index d698b295e6d..ce68e433ab8 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -85,14 +85,15 @@ class IssuableBaseService < BaseService def find_or_create_label_ids labels = params.delete(:labels) + return unless labels - params[:label_ids] = labels.split(',').map do |label_name| + params[:label_ids] = labels.split(",").map do |label_name| service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip) label = service.execute - label.id - end + label.try(:id) + end.compact end def process_label_ids(attributes, existing_label_ids: nil) @@ -140,6 +141,7 @@ class IssuableBaseService < BaseService params.delete(:state_event) params[:author] ||= current_user + label_ids = process_label_ids(params) issuable.assign_attributes(params) diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb index d622f9edd33..cf4f7606c94 100644 --- a/app/services/labels/find_or_create_service.rb +++ b/app/services/labels/find_or_create_service.rb @@ -22,9 +22,14 @@ module Labels ).execute(skip_authorization: skip_authorization) end + # Only creates the label if current_user can do so, if the label does not exist + # and the user can not create the label, nil is returned def find_or_create_label new_label = available_labels.find_by(title: title) - new_label ||= project.labels.create(params) + + if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project)) + new_label = project.labels.create(params) + end new_label end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index dd0d738674e..bebfca7537b 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -81,7 +81,7 @@ module MergeRequests commit = commits.first merge_request.title = commit.title merge_request.description ||= commit.description.try(:strip) - elsif iid && (issue = merge_request.target_project.get_issue(iid)) && !issue.try(:confidential?) + elsif iid && issue = merge_request.target_project.get_issue(iid, current_user) case issue when Issue merge_request.title = "Resolve \"#{issue.title}\"" diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index ce803f329f9..7accd2529af 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -443,7 +443,16 @@ Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. - + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :html_emails_enabled do + = f.check_box :html_emails_enabled + Enable HTML emails + .help-block + By default GitLab sends emails in HTML and plain text formats so mail + clients can choose what format to use. Disable this option if you only + want to send emails in plain text format. %fieldset %legend Automatic Git repository housekeeping .form-group diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index f7abad54286..48b0fd504f4 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -4,13 +4,13 @@ %ul.nav-links = nav_link(page: [dashboard_projects_path, root_path]) do = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do - Your Projects + Your projects = nav_link(page: starred_dashboard_projects_path) do = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do - Starred Projects + Starred projects = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do - Explore Projects + Explore projects .nav-controls = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml index fdea834ff45..4a55aac0df6 100644 --- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml @@ -4,6 +4,18 @@ Welcome to GitLab %p.blank-state-text Code, test, and deploy together + +- if current_user.can_create_group? + .blank-state + .blank-state-icon + = custom_icon("group", size: 50) + %h3.blank-state-title + You can create a group for several dependent projects. + %p.blank-state-text + Groups are the best way to manage projects and members. + = link_to new_group_path, class: "btn btn-new" do + New group + .blank-state .blank-state-icon = custom_icon("project", size: 50) @@ -21,17 +33,6 @@ = link_to new_project_path, class: "btn btn-new" do New project -- if current_user.can_create_group? - .blank-state - .blank-state-icon - = custom_icon("group", size: 50) - %h3.blank-state-title - You can create a group for several dependent projects. - %p.blank-state-text - Groups are the best way to manage projects and members. - = link_to new_group_path, class: "btn btn-new" do - New group - -if publicish_project_count > 0 .blank-state .blank-state-icon diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 472d698486b..62f52086be4 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -50,13 +50,13 @@ data: { data: todo_actions_options }}) .pull-right .dropdown.inline.prepend-left-10 - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'} %span.light - if @sort.present? = sort_options_hash[@sort] - else = sort_title_recently_created - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort %li = link_to todos_filter_path(sort: sort_value_priority) do diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index a1b39d9e1a0..4e5d965ccbe 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -17,13 +17,13 @@ .pull-right .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'} %span.light - if @sort.present? = sort_options_hash[@sort] - else = sort_title_recently_created - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to explore_groups_path(sort: sort_value_recently_created) do diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index 4cff14b096b..5ea154c36b4 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -1,13 +1,13 @@ - if current_user .dropdown - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %button.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} = icon('globe') %span.light Visibility: - if params[:visibility_level].present? = visibility_level_label(params[:visibility_level].to_i) - else Any - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_projects_path(visibility_level: nil) do @@ -20,14 +20,14 @@ - if @tags.present? .dropdown - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %button.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} = icon('tags') %span.light Tags: - if params[:tag].present? = params[:tag] - else Any - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_projects_path(tag: nil) do diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 99a58bbb676..701bcd3ab71 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -70,7 +70,7 @@ %span Issues - if @project.default_issues_tracker? - %span.badge.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count) + %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) - if project_nav_tab? :merge_requests = nav_link(controller: :merge_requests) do diff --git a/app/views/notify/_note_message.text.erb b/app/views/notify/_note_message.text.erb new file mode 100644 index 00000000000..f82cbc9a3fc --- /dev/null +++ b/app/views/notify/_note_message.text.erb @@ -0,0 +1,5 @@ +<% if current_application_settings.email_author_in_body %> + <%= @note.author_name %> wrote: +<% end -%> + +<%= @note.note %> diff --git a/app/views/notify/_note_mr_or_commit_email.html.haml b/app/views/notify/_note_mr_or_commit_email.html.haml new file mode 100644 index 00000000000..edf8dfe7e9e --- /dev/null +++ b/app/views/notify/_note_mr_or_commit_email.html.haml @@ -0,0 +1,18 @@ += content_for :head do + = stylesheet_link_tag 'mailers/highlighted_diff_email' + +New comment + +- if @discussion && @discussion.diff_file + on + = link_to @note.diff_file.file_path, @target_url, class: 'details' + \: + %table + = render partial: "projects/diffs/line", + collection: @discussion.truncated_diff_lines, + as: :line, + locals: { diff_file: @note.diff_file, + plain: true, + email: true } + += render 'note_message' diff --git a/app/views/notify/_note_mr_or_commit_email.text.erb b/app/views/notify/_note_mr_or_commit_email.text.erb new file mode 100644 index 00000000000..b4fcdf6b1e9 --- /dev/null +++ b/app/views/notify/_note_mr_or_commit_email.text.erb @@ -0,0 +1,8 @@ +<% if @discussion && @discussion.diff_file -%> + on <%= @note.diff_file.file_path -%> +<% end -%>: + +<%= url %> + +<%= render 'simple_diff' if @discussion -%> +<%= render 'note_message' %> diff --git a/app/views/notify/_simple_diff.text.erb b/app/views/notify/_simple_diff.text.erb new file mode 100644 index 00000000000..c28d1cc34d3 --- /dev/null +++ b/app/views/notify/_simple_diff.text.erb @@ -0,0 +1,3 @@ +<% @discussion.truncated_diff_lines(highlight: false).each do |line| %> +> <%= line.text %> +<% end %> diff --git a/app/views/notify/note_commit_email.html.haml b/app/views/notify/note_commit_email.html.haml index 1d961e4424c..0a650e3b2ca 100644 --- a/app/views/notify/note_commit_email.html.haml +++ b/app/views/notify/note_commit_email.html.haml @@ -1,2 +1,2 @@ -= render 'note_message' - +%p.details + = render 'note_mr_or_commit_email' diff --git a/app/views/notify/note_commit_email.text.erb b/app/views/notify/note_commit_email.text.erb index aaeaf5fdf73..6aa085a172e 100644 --- a/app/views/notify/note_commit_email.text.erb +++ b/app/views/notify/note_commit_email.text.erb @@ -1,9 +1,2 @@ -New comment for Commit <%= @commit.short_id %> - -<%= url_for(namespace_project_commit_url(@note.project.namespace, @note.project, id: @commit.id, anchor: "note_#{@note.id}")) %> - - -Author: <%= @note.author_name %> - -<%= @note.note %> - +New comment for Commit <%= @commit.short_id -%> +<%= render partial: 'note_mr_or_commit_email', locals: { url: @target_url } %> diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml index ea7e3d199fd..0a650e3b2ca 100644 --- a/app/views/notify/note_merge_request_email.html.haml +++ b/app/views/notify/note_merge_request_email.html.haml @@ -1,7 +1,2 @@ -- if @note.diff_note? && @note.diff_file - %p.details - New comment on diff for - = link_to @note.diff_file.file_path, @target_url - \: - -= render 'note_message' +%p.details + = render 'note_mr_or_commit_email' diff --git a/app/views/notify/note_merge_request_email.text.erb b/app/views/notify/note_merge_request_email.text.erb index 8cdab63829e..2ce64c494cf 100644 --- a/app/views/notify/note_merge_request_email.text.erb +++ b/app/views/notify/note_merge_request_email.text.erb @@ -1,9 +1,2 @@ -New comment for Merge Request <%= @merge_request.to_reference %> - -<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %> - - -<%= @note.author_name %> - -<%= @note.note %> - +New comment for Merge Request <%= @merge_request.to_reference -%> +<%= render partial: 'note_mr_or_commit_email', locals: { url: @target_url }%> diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index 307c5a11206..25883de257c 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -1,5 +1,5 @@ = content_for :head do - = stylesheet_link_tag 'mailers/repository_push_email' + = stylesheet_link_tag 'mailers/highlighted_diff_email' %h3 #{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name} diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 2a0352a71b7..a5dcd93f42e 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -27,5 +27,5 @@ = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}" = hidden_field_tag 'last_commit_sha', @last_commit_sha = hidden_field_tag 'content', '', id: "file-content" - = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] + = hidden_field_tag 'from_merge_request_iid', params[:from_merge_request_iid] = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id) diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 2246316b540..5fd664c7a93 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -12,10 +12,10 @@ = search_field_tag :search, params[:search], { placeholder: 'Filter by branch name', id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false } .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'} %span.light = projects_sort_options_hash[@sort] - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_branches_path(sort: sort_value_name) do diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index f5562046953..d5004f6a066 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -116,7 +116,7 @@ .title Stage %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'} %span.stage-selection More - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu - @build.pipeline.stages.each do |stage| %li diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 120ba9ffcd2..6c33d80becd 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -9,7 +9,7 @@ = icon('comment') \ - if editable_diff?(diff_file) - - link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {} + - link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {} = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, blob: blob, link_opts: link_opts) diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index a3e4b5b777e..16c96b66714 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -25,7 +25,7 @@ %a{href: "##{line_code}", data: { linenumber: link_text }} %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }< - if email - %pre= diff_line_content(line.text) + %pre= line.text - else = diff_line_content(line.text) diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index abf4f697f86..5ee3979c7e7 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -9,13 +9,13 @@ spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' } .dropdown - %button.dropdown-toggle.btn.sort-forks{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'} %span.light sort: - if @sort.present? = sort_options_hash[@sort] - else = sort_title_recently_created - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li - excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id] diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 18c72ed875c..6d9b91ad0e7 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,7 +1,7 @@ - if @pipeline .mr-widget-heading - %w[success success_with_warnings skipped canceled failed running pending].each do |status| - .ci_widget{ class: "ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) } + .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) } = ci_icon_for_status(status) %span Pipeline diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index b43b13de4ca..1d39f3a7534 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -12,10 +12,10 @@ = search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false } .dropdown.inline - %button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} } + %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown'} } %span.light = projects_sort_options_hash[@sort] - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li = link_to filter_tags_path(sort: sort_value_name) do diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml index 07cee86ba4c..c7cebf45160 100644 --- a/app/views/projects/variables/_table.html.haml +++ b/app/views/projects/variables/_table.html.haml @@ -12,8 +12,8 @@ - @project.variables.order_key_asc.each do |variable| - if variable.id? %tr - %td= variable.key - %td= variable.value + %td.variable-key= variable.key + %td.variable-value{ "data-value" => variable.value }****** %td = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do %span.sr-only diff --git a/app/views/projects/variables/index.html.haml b/app/views/projects/variables/index.html.haml index 09bb54600af..39303700131 100644 --- a/app/views/projects/variables/index.html.haml +++ b/app/views/projects/variables/index.html.haml @@ -15,3 +15,4 @@ No variables found, add one with the form above. - else = render "table" + %button.btn.btn-info.js-btn-toggle-reveal-values{"data-status" => 'hidden'} Reveal Values diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml index ef1c0296d49..938be20c7cf 100644 --- a/app/views/search/_filter.html.haml +++ b/app/views/search/_filter.html.haml @@ -3,7 +3,7 @@ - if params[:project_id].present? = hidden_field_tag :project_id, params[:project_id] .dropdown - %button.dropdown-menu-toggle.btn.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Group:" } } + %button.dropdown-menu-toggle.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Group:" } } %span.dropdown-toggle-text Group: - if @group.present? @@ -18,7 +18,7 @@ = dropdown_loading .dropdown.project-filter - %button.dropdown-menu-toggle.btn.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Project:" } } + %button.dropdown-menu-toggle.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Project:" } } %span.dropdown-toggle-text Project: - if @project.present? diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index 68e05cb72e1..ede3c7090d7 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -1,11 +1,11 @@ .dropdown.inline.prepend-left-10 - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle{type: 'button', data: {toggle: 'dropdown'}} %span.light - if @sort.present? = sort_options_hash[@sort] - else = sort_title_recently_created - = icon('caret-down') + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort %li = link_to page_filter_path(sort: sort_value_priority, label: true) do diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 9b9ad510444..3d515a05d46 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -16,20 +16,9 @@ = render 'shared/issuable/form/template_selector', issuable: issuable = render 'shared/issuable/form/title', issuable: issuable, form: form -.form-group.detail-page-description - = form.label :description, 'Description', class: 'control-label' - .col-sm-10 += render 'shared/issuable/form/description', issuable: issuable, form: form - = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: form, attr: :description, - classes: 'note-textarea', - placeholder: "Write a comment or drag your files here...", - supports_slash_commands: !issuable.persisted? - = render 'projects/notes/hints', supports_slash_commands: !issuable.persisted? - .clearfix - .error-alert - -- if issuable.is_a?(Issue) +- if issuable.respond_to?(:confidential) .form-group .col-sm-offset-2.col-sm-10 .checkbox @@ -37,38 +26,7 @@ = form.check_box :confidential This issue is confidential and should only be visible to team members with at least Reporter access. -- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) - - has_due_date = issuable.has_attribute?(:due_date) - %hr - .row - %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") } - .form-group.issue-assignee - = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" - .col-sm-10{ class: ("col-lg-8" if has_due_date) } - .issuable-form-select-holder - - if issuable.assignee_id - = form.hidden_field :assignee_id - = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", - placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) - .form-group.issue-milestone - = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" - .col-sm-10{ class: ("col-lg-8" if has_due_date) } - .issuable-form-select-holder - = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" - .form-group - - has_labels = @labels && @labels.any? - = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" - = form.hidden_field :label_ids, multiple: true, value: '' - .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } - .issuable-form-select-holder - = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label" - - if has_due_date - .col-lg-6 - .form-group - = form.label :due_date, "Due date", class: "control-label" - .col-sm-10 - .issuable-form-select-holder - = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" += render 'shared/issuable/form/metadata', issuable: issuable, form: form - if issuable.can_move?(current_user) %hr diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 1d778bc88de..22b5a6aa11b 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -22,7 +22,7 @@ %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data} %span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) } = multi_label_name(selected, "Labels") - = icon('caret-down') + = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable = render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create } - if show_create && project && can?(current_user, :admin_label, project) diff --git a/app/views/shared/issuable/form/_description.html.haml b/app/views/shared/issuable/form/_description.html.haml new file mode 100644 index 00000000000..dbace9ce401 --- /dev/null +++ b/app/views/shared/issuable/form/_description.html.haml @@ -0,0 +1,15 @@ +- issuable = local_assigns.fetch(:issuable) +- form = local_assigns.fetch(:form) + +.form-group.detail-page-description + = form.label :description, 'Description', class: 'control-label' + .col-sm-10 + + = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do + = render 'projects/zen', f: form, attr: :description, + classes: 'note-textarea', + placeholder: "Write a comment or drag your files here...", + supports_slash_commands: !issuable.persisted? + = render 'projects/notes/hints', supports_slash_commands: !issuable.persisted? + .clearfix + .error-alert diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml new file mode 100644 index 00000000000..a47085230b8 --- /dev/null +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -0,0 +1,38 @@ +- issuable = local_assigns.fetch(:issuable) + +- return unless can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) + +- has_due_date = issuable.has_attribute?(:due_date) +- has_labels = @labels && @labels.any? +- form = local_assigns.fetch(:form) + +%hr +.row + %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") } + .form-group.issue-assignee + = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" + .col-sm-10{ class: ("col-lg-8" if has_due_date) } + .issuable-form-select-holder + - if issuable.assignee_id + = form.hidden_field :assignee_id + = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", + placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) + .form-group.issue-milestone + = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" + .col-sm-10{ class: ("col-lg-8" if has_due_date) } + .issuable-form-select-holder + = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" + .form-group + - has_labels = @labels && @labels.any? + = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" + = form.hidden_field :label_ids, multiple: true, value: '' + .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } + .issuable-form-select-holder + = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label" + - if has_due_date + .col-lg-6 + .form-group + = form.label :due_date, "Due date", class: "control-label" + .col-sm-10 + .issuable-form-select-holder + = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" diff --git a/changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml b/changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml new file mode 100644 index 00000000000..855e4e1ba1d --- /dev/null +++ b/changelogs/unreleased/24135-new-project-should-be-below-new-group-on-the-welcome-screen.yml @@ -0,0 +1,4 @@ +--- +title: Moved new projects button below new group button on the welcome screen +merge_request: 7770 +author: diff --git a/changelogs/unreleased/24150-consistent-dropdown-styles.yml b/changelogs/unreleased/24150-consistent-dropdown-styles.yml new file mode 100644 index 00000000000..a328d796c43 --- /dev/null +++ b/changelogs/unreleased/24150-consistent-dropdown-styles.yml @@ -0,0 +1,4 @@ +--- +title: Homogenize filter and sort dropdown look'n'feel +merge_request: 7583 +author: David Wagner diff --git a/changelogs/unreleased/24161-non-intuitive-buttons-for-import-sources-in-administrator-settings-enable-disable.yml b/changelogs/unreleased/24161-non-intuitive-buttons-for-import-sources-in-administrator-settings-enable-disable.yml deleted file mode 100644 index 1404748e83e..00000000000 --- a/changelogs/unreleased/24161-non-intuitive-buttons-for-import-sources-in-administrator-settings-enable-disable.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Changed import sources buttons to checkboxes -merge_request: 7598 -author: Luke "Jared" Bennett diff --git a/changelogs/unreleased/24266-Afraid-to-press-the-Orange-button-on-Merge-request-screen.yml b/changelogs/unreleased/24266-Afraid-to-press-the-Orange-button-on-Merge-request-screen.yml deleted file mode 100644 index 28ca20c7dcc..00000000000 --- a/changelogs/unreleased/24266-Afraid-to-press-the-Orange-button-on-Merge-request-screen.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: If Build running change accept merge request when build succeeds button from orange to blue -merge_request: 7577 -author: diff --git a/changelogs/unreleased/24739-collapsed-build-list-sorting.yml b/changelogs/unreleased/24739-collapsed-build-list-sorting.yml deleted file mode 100644 index 036e606318f..00000000000 --- a/changelogs/unreleased/24739-collapsed-build-list-sorting.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Sort builds by name within pipeline graph -merge_request: 7681 -author: diff --git a/changelogs/unreleased/24779-last-deployment-call-on-nil-environment-fix.yml b/changelogs/unreleased/24779-last-deployment-call-on-nil-environment-fix.yml deleted file mode 100644 index 5e7580fb8f2..00000000000 --- a/changelogs/unreleased/24779-last-deployment-call-on-nil-environment-fix.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: fixes last_deployment call environment is nil -merge_request: 7671 -author: diff --git a/changelogs/unreleased/24804-wrong-render-index-should-be-render-show-in-projects-pipelinessettingscontroller-update.yml b/changelogs/unreleased/24804-wrong-render-index-should-be-render-show-in-projects-pipelinessettingscontroller-update.yml deleted file mode 100644 index 92dbbe3d164..00000000000 --- a/changelogs/unreleased/24804-wrong-render-index-should-be-render-show-in-projects-pipelinessettingscontroller-update.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix wrong template rendered when CI/CD settings aren't update successfully -merge_request: 7665 -author: diff --git a/changelogs/unreleased/24863-mrs-without-discussions-are-mergeable.yml b/changelogs/unreleased/24863-mrs-without-discussions-are-mergeable.yml deleted file mode 100644 index 9bdb9411135..00000000000 --- a/changelogs/unreleased/24863-mrs-without-discussions-are-mergeable.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Correctly determine mergeability of MR with no discussions -merge_request: -author: diff --git a/changelogs/unreleased/24999-fix-project-avatar-alignment.yml b/changelogs/unreleased/24999-fix-project-avatar-alignment.yml new file mode 100644 index 00000000000..7af812e7359 --- /dev/null +++ b/changelogs/unreleased/24999-fix-project-avatar-alignment.yml @@ -0,0 +1,4 @@ +--- +title: Adjust the width of project avatars to fix alignment within their container +merge_request: +author: Ryan Harris diff --git a/changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml b/changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml new file mode 100644 index 00000000000..cc8b0e28277 --- /dev/null +++ b/changelogs/unreleased/25002-sentence-case-dashboard-tabs.yml @@ -0,0 +1,4 @@ +--- +title: Sentence cased the nav tab headers on the project dashboard page +merge_request: +author: Ryan Harris diff --git a/changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml b/changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml new file mode 100644 index 00000000000..2c3ba1dfe44 --- /dev/null +++ b/changelogs/unreleased/25011-hoverstates-for-collapsed-issue-merge-request-sidebar.yml @@ -0,0 +1,4 @@ +--- +title: Adds hoverstates for collapsed Issue/Merge Request sidebar +merge_request: !7777 +author: diff --git a/changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml b/changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml new file mode 100644 index 00000000000..a7b5810f1bf --- /dev/null +++ b/changelogs/unreleased/25026-authenticate-user-for-new-snippet.yml @@ -0,0 +1,4 @@ +--- +title: Redirect to sign-in page when unauthenticated user tries to create a snippet +merge_request: 7786 +author: diff --git a/changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml b/changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml new file mode 100644 index 00000000000..dad9db0ffef --- /dev/null +++ b/changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml @@ -0,0 +1,4 @@ +--- +title: Fix pipelines info being hidden in merge request widget +merge_request: 7808 +author: diff --git a/changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml b/changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml new file mode 100644 index 00000000000..9dd04d3f089 --- /dev/null +++ b/changelogs/unreleased/7749-add-setting-to-disable-html-emails.yml @@ -0,0 +1,3 @@ +title: Add setting to enable/disable HTML emails +merge_request: 7749 +author: diff --git a/changelogs/unreleased/Last-minute-CI-Style-tweaks-for-8-14.yml b/changelogs/unreleased/Last-minute-CI-Style-tweaks-for-8-14.yml deleted file mode 100644 index 7d49c639a43..00000000000 --- a/changelogs/unreleased/Last-minute-CI-Style-tweaks-for-8-14.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Last minute CI Style tweaks for 8.14 -merge_request: 7643 -author: diff --git a/changelogs/unreleased/disable-calendar-deselection.yml b/changelogs/unreleased/disable-calendar-deselection.yml deleted file mode 100644 index 060797bba34..00000000000 --- a/changelogs/unreleased/disable-calendar-deselection.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix deselecting calendar days on contribution graph -merge_request: 6453 -author: ClemMakesApps diff --git a/changelogs/unreleased/fix-build-without-trace-exceptions.yml b/changelogs/unreleased/fix-build-without-trace-exceptions.yml deleted file mode 100644 index 3b95e96e212..00000000000 --- a/changelogs/unreleased/fix-build-without-trace-exceptions.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix exceptions when loading build trace -merge_request: 7658 -author: diff --git a/changelogs/unreleased/fix-cycle-analytics-plan-issue.yml b/changelogs/unreleased/fix-cycle-analytics-plan-issue.yml deleted file mode 100644 index 6ed16c6d722..00000000000 --- a/changelogs/unreleased/fix-cycle-analytics-plan-issue.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix cycle analytics plan stage when commits are missing -merge_request: -author: diff --git a/changelogs/unreleased/fix_sidekiq_stats_in_admin_area.yml b/changelogs/unreleased/fix_sidekiq_stats_in_admin_area.yml deleted file mode 100644 index 4f007be8624..00000000000 --- a/changelogs/unreleased/fix_sidekiq_stats_in_admin_area.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Sidekiq stats in the admin area will now show correctly on different platforms -merge_request: -author: blackst0ne diff --git a/changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml b/changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml new file mode 100644 index 00000000000..73d8a52e001 --- /dev/null +++ b/changelogs/unreleased/hoopes-gitlab-ce-21027-add-diff-hunks-to-notification-emails.yml @@ -0,0 +1,4 @@ +--- +title: Add git diff context to notifications of new notes on merge requests +merge_request: +author: Heidi Hoopes diff --git a/changelogs/unreleased/issue-boards-dragging-fix.yml b/changelogs/unreleased/issue-boards-dragging-fix.yml deleted file mode 100644 index 565e09b930b..00000000000 --- a/changelogs/unreleased/issue-boards-dragging-fix.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed issue boards dragging card removing random issues -merge_request: -author: diff --git a/changelogs/unreleased/jej-22869.yml b/changelogs/unreleased/jej-22869.yml new file mode 100644 index 00000000000..9d2edcfee42 --- /dev/null +++ b/changelogs/unreleased/jej-22869.yml @@ -0,0 +1,4 @@ +--- +title: Fix information disclosure in `Projects::BlobController#update` +merge_request: +author: diff --git a/changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml b/changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml new file mode 100644 index 00000000000..844fba9a107 --- /dev/null +++ b/changelogs/unreleased/jej-fix-missing-access-check-on-issues.yml @@ -0,0 +1,4 @@ +--- +title: Fix missing access checks on issue lookup using IssuableFinder +merge_request: +author: diff --git a/changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml b/changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml new file mode 100644 index 00000000000..c0b6f50052c --- /dev/null +++ b/changelogs/unreleased/jej-use-issuable-finder-instead-of-access-check.yml @@ -0,0 +1,4 @@ +--- +title: Replace issue access checks with use of IssuableFinder +merge_request: +author: diff --git a/changelogs/unreleased/readme-link-fix.yml b/changelogs/unreleased/readme-link-fix.yml new file mode 100644 index 00000000000..211d3b80c3a --- /dev/null +++ b/changelogs/unreleased/readme-link-fix.yml @@ -0,0 +1,4 @@ +--- +title: Fix broken README.md UX guide link. +merge_request: +author: diff --git a/changelogs/unreleased/remove-jsx-react-eslint-plugins.yml b/changelogs/unreleased/remove-jsx-react-eslint-plugins.yml new file mode 100644 index 00000000000..6e02998b3a8 --- /dev/null +++ b/changelogs/unreleased/remove-jsx-react-eslint-plugins.yml @@ -0,0 +1,5 @@ +--- +title: Changed eslint airbnb config to the base airbnb config and corrected eslintrc + plugins and envs +merge_request: 7470 +author: Luke "Jared" Bennett diff --git a/changelogs/unreleased/removing_unnecessary_indexes.yml b/changelogs/unreleased/removing_unnecessary_indexes.yml new file mode 100644 index 00000000000..01314ab5585 --- /dev/null +++ b/changelogs/unreleased/removing_unnecessary_indexes.yml @@ -0,0 +1,4 @@ +--- +title: Remove unnecessary database indices +merge_request: +author: diff --git a/changelogs/unreleased/zj-fix-label-creation-non-members.yml b/changelogs/unreleased/zj-fix-label-creation-non-members.yml new file mode 100644 index 00000000000..ae4824f82fa --- /dev/null +++ b/changelogs/unreleased/zj-fix-label-creation-non-members.yml @@ -0,0 +1,4 @@ +--- +title: Non members cannot create labels through the API +merge_request: +author: diff --git a/changelogs/unreleased/zj-issue-search-slash-command.yml b/changelogs/unreleased/zj-issue-search-slash-command.yml new file mode 100644 index 00000000000..de41c39d545 --- /dev/null +++ b/changelogs/unreleased/zj-issue-search-slash-command.yml @@ -0,0 +1,4 @@ +--- +title: Add issue search slash command +merge_request: +author: diff --git a/changelogs/unreleased/zj-upgrade-grape.yml b/changelogs/unreleased/zj-upgrade-grape.yml deleted file mode 100644 index 1df42d98733..00000000000 --- a/changelogs/unreleased/zj-upgrade-grape.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update grape entity to 0.6.0 -merge_request: 7491 -author: diff --git a/config/initializers/email_template_interceptor.rb b/config/initializers/email_template_interceptor.rb new file mode 100644 index 00000000000..f195ca9bcd6 --- /dev/null +++ b/config/initializers/email_template_interceptor.rb @@ -0,0 +1,2 @@ +# Interceptor in lib/email_template_interceptor.rb +ActionMailer::Base.register_interceptor(EmailTemplateInterceptor) diff --git a/db/migrate/20161128142110_remove_unnecessary_indexes.rb b/db/migrate/20161128142110_remove_unnecessary_indexes.rb new file mode 100644 index 00000000000..9deab19782e --- /dev/null +++ b/db/migrate/20161128142110_remove_unnecessary_indexes.rb @@ -0,0 +1,33 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveUnnecessaryIndexes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + DOWNTIME = false + + def up + remove_index :labels, column: :group_id if index_exists?(:labels, :group_id) + remove_index :award_emoji, column: :user_id if index_exists?(:award_emoji, :user_id) + remove_index :ci_builds, column: :commit_id if index_exists?(:ci_builds, :commit_id) + remove_index :deployments, column: :project_id if index_exists?(:deployments, :project_id) + remove_index :deployments, column: ["project_id", "environment_id"] if index_exists?(:deployments, ["project_id", "environment_id"]) + remove_index :lists, column: :board_id if index_exists?(:lists, :board_id) + remove_index :milestones, column: :project_id if index_exists?(:milestones, :project_id) + remove_index :notes, column: :project_id if index_exists?(:notes, :project_id) + remove_index :users_star_projects, column: :user_id if index_exists?(:users_star_projects, :user_id) + end + + def down + add_concurrent_index :labels, :group_id + add_concurrent_index :award_emoji, :user_id + add_concurrent_index :ci_builds, :commit_id + add_concurrent_index :deployments, :project_id + add_concurrent_index :deployments, ["project_id", "environment_id"] + add_concurrent_index :lists, :board_id + add_concurrent_index :milestones, :project_id + add_concurrent_index :notes, :project_id + add_concurrent_index :users_star_projects, :user_id + end +end diff --git a/db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb b/db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb new file mode 100644 index 00000000000..1c59241d0fe --- /dev/null +++ b/db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb @@ -0,0 +1,29 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddHtmlEmailsEnabledToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + add_column :application_settings, :html_emails_enabled, :boolean, default: true + end +end diff --git a/db/schema.rb b/db/schema.rb index b3c49b52597..0d510c8a269 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161118183841) do +ActiveRecord::Schema.define(version: 20161128161412) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -106,6 +106,7 @@ ActiveRecord::Schema.define(version: 20161118183841) do t.integer "housekeeping_incremental_repack_period", default: 10, null: false t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_gc_period", default: 200, null: false + t.boolean "html_emails_enabled", default: true end create_table "audit_events", force: :cascade do |t| @@ -131,7 +132,6 @@ ActiveRecord::Schema.define(version: 20161118183841) do add_index "award_emoji", ["awardable_type", "awardable_id"], name: "index_award_emoji_on_awardable_type_and_awardable_id", using: :btree add_index "award_emoji", ["user_id", "name"], name: "index_award_emoji_on_user_id_and_name", using: :btree - add_index "award_emoji", ["user_id"], name: "index_award_emoji_on_user_id", using: :btree create_table "boards", force: :cascade do |t| t.integer "project_id", null: false @@ -219,7 +219,6 @@ ActiveRecord::Schema.define(version: 20161118183841) do add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree - add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree add_index "ci_builds", ["gl_project_id"], name: "index_ci_builds_on_gl_project_id", using: :btree add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree @@ -409,9 +408,7 @@ ActiveRecord::Schema.define(version: 20161118183841) do end add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree - add_index "deployments", ["project_id", "environment_id"], name: "index_deployments_on_project_id_and_environment_id", using: :btree add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree - add_index "deployments", ["project_id"], name: "index_deployments_on_project_id", using: :btree create_table "emails", force: :cascade do |t| t.integer "user_id", null: false @@ -569,7 +566,6 @@ ActiveRecord::Schema.define(version: 20161118183841) do end add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree - add_index "labels", ["group_id"], name: "index_labels_on_group_id", using: :btree create_table "lfs_objects", force: :cascade do |t| t.string "oid", null: false @@ -600,7 +596,6 @@ ActiveRecord::Schema.define(version: 20161118183841) do end add_index "lists", ["board_id", "label_id"], name: "index_lists_on_board_id_and_label_id", unique: true, using: :btree - add_index "lists", ["board_id"], name: "index_lists_on_board_id", using: :btree add_index "lists", ["label_id"], name: "index_lists_on_label_id", using: :btree create_table "members", force: :cascade do |t| @@ -726,7 +721,6 @@ ActiveRecord::Schema.define(version: 20161118183841) do add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree - add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} @@ -789,7 +783,6 @@ ActiveRecord::Schema.define(version: 20161118183841) do add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree - add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree create_table "notification_settings", force: :cascade do |t| @@ -1242,7 +1235,6 @@ ActiveRecord::Schema.define(version: 20161118183841) do add_index "users_star_projects", ["project_id"], name: "index_users_star_projects_on_project_id", using: :btree add_index "users_star_projects", ["user_id", "project_id"], name: "index_users_star_projects_on_user_id_and_project_id", unique: true, using: :btree - add_index "users_star_projects", ["user_id"], name: "index_users_star_projects_on_user_id", using: :btree create_table "web_hooks", force: :cascade do |t| t.string "url", limit: 2000 diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index d3f216fb3bf..b8b63df091e 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -221,7 +221,7 @@ Tip: If you want to limit access to the nested members of an Active Directory group you can use the following syntax: ``` -(memberOf:1.2.840.113556.1.4.1941:=CN=My Group,DC=Example,DC=com) +(memberOf=CN=My Group,DC=Example,DC=com) ``` Please note that GitLab does not support the custom filter syntax used by diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md index b7e6387838e..568dedf1669 100644 --- a/doc/development/limit_ee_conflicts.md +++ b/doc/development/limit_ee_conflicts.md @@ -143,109 +143,162 @@ to resolve when you add the indentation to the equation. For instance this kind of thing: ```haml +.form-group.detail-page-description + = form.label :description, 'Description', class: 'control-label' + .col-sm-10 + = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do + = render 'projects/zen', f: form, attr: :description, + classes: 'note-textarea', + placeholder: "Write a comment or drag your files here...", + supports_slash_commands: !issuable.persisted? + = render 'projects/notes/hints', supports_slash_commands: !issuable.persisted? + .clearfix + .error-alert +- if issuable.is_a?(Issue) + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = form.label :confidential do + = form.check_box :confidential + This issue is confidential and should only be visible to team members with at least Reporter access. - if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) - has_due_date = issuable.has_attribute?(:due_date) %hr .row %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") } .form-group.issue-assignee - = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" + = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder - if issuable.assignee_id - = f.hidden_field :assignee_id + = form.hidden_field :assignee_id = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) .form-group.issue-milestone - = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" + = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" .form-group - has_labels = @labels && @labels.any? - = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" - = f.hidden_field :label_ids, multiple: true, value: '' + = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" + = form.hidden_field :label_ids, multiple: true, value: '' .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } .issuable-form-select-holder - = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false, show_menu_above: 'true' }, dropdown_title: "Select label" - + = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label" - if issuable.respond_to?(:weight) + - weight_options = Issue.weight_options + - weight_options.delete(Issue::WEIGHT_ALL) + - weight_options.delete(Issue::WEIGHT_ANY) .form-group - = f.label :label_ids, class: "control-label #{"col-lg-4" if has_due_date}" do + = form.label :label_ids, class: "control-label #{"col-lg-4" if has_due_date}" do Weight .col-sm-10{ class: ("col-lg-8" if has_due_date) } - = f.select :weight, issues_weight_options(issuable.weight, edit: true), { include_blank: true }, - { class: 'select2 js-select2', data: { placeholder: "Select weight" }} - + .issuable-form-select-holder + - if issuable.weight + = form.hidden_field :weight + = dropdown_tag(issuable.weight || "Weight", options: { title: "Select weight", toggle_class: 'js-weight-select js-issuable-form-weight', dropdown_class: "dropdown-menu-selectable dropdown-menu-weight", + placeholder: "Search weight", data: { field_name: "#{issuable.class.model_name.param_key}[weight]" , default_label: "Weight" } }) do + %ul + - weight_options.each do |weight| + %li + %a{href: "#", data: { id: weight, none: weight === Issue::WEIGHT_NONE }, class: ("is-active" if issuable.weight == weight)} + = weight - if has_due_date .col-lg-6 .form-group - = f.label :due_date, "Due date", class: "control-label" + = form.label :due_date, "Due date", class: "control-label" .col-sm-10 .issuable-form-select-holder - = f.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" + = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" ``` could be simplified by using partials: ```haml -= render 'metadata_form', issuable: issuable += render 'shared/issuable/form/description', issuable: issuable, form: form + +- if issuable.respond_to?(:confidential) + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = form.label :confidential do + = form.check_box :confidential + This issue is confidential and should only be visible to team members with at least Reporter access. + += render 'shared/issuable/form/metadata', issuable: issuable, form: form ``` -and then the `_metadata_form.html.haml` could be as follows: +and then the `app/views/shared/issuable/form/_metadata.html.haml` could be as follows: ```haml +- issuable = local_assigns.fetch(:issuable) + - return unless can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) - has_due_date = issuable.has_attribute?(:due_date) +- has_labels = @labels && @labels.any? +- form = local_assigns.fetch(:form) + %hr .row %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") } .form-group.issue-assignee - = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" + = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder - if issuable.assignee_id - = f.hidden_field :assignee_id + = form.hidden_field :assignee_id = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", - placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) + placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) .form-group.issue-milestone - = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" + = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" .form-group - has_labels = @labels && @labels.any? - = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" - = f.hidden_field :label_ids, multiple: true, value: '' + = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" + = form.hidden_field :label_ids, multiple: true, value: '' .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } .issuable-form-select-holder - = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false, show_menu_above: 'true' }, dropdown_title: "Select label" + = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label" - = render 'weight_form', issuable: issuable, has_due_date: has_due_date + = render "shared/issuable/form/weight", issuable: issuable, form: form - if has_due_date .col-lg-6 .form-group - = f.label :due_date, "Due date", class: "control-label" + = form.label :due_date, "Due date", class: "control-label" .col-sm-10 .issuable-form-select-holder - = f.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" + = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" ``` -and then the `_weight_form.html.haml` could be as follows: +and then the `app/views/shared/issuable/form/_weight.html.haml` could be as follows: ```haml +- issuable = local_assigns.fetch(:issuable) + - return unless issuable.respond_to?(:weight) - has_due_date = issuable.has_attribute?(:due_date) +- form = local_assigns.fetch(:form) .form-group - = f.label :label_ids, class: "control-label #{"col-lg-4" if has_due_date}" do + = form.label :label_ids, class: "control-label #{"col-lg-4" if has_due_date}" do Weight .col-sm-10{ class: ("col-lg-8" if has_due_date) } - = f.select :weight, issues_weight_options(issuable.weight, edit: true), { include_blank: true }, - { class: 'select2 js-select2', data: { placeholder: "Select weight" }} + .issuable-form-select-holder + - if issuable.weight + = form.hidden_field :weight + + = weight_dropdown_tag(issuable, toggle_class: 'js-issuable-form-weight') do + %ul + - Issue.weight_options.each do |weight| + %li + %a{ href: '#', data: { id: weight, none: weight === Issue::WEIGHT_NONE }, class: ("is-active" if issuable.weight == weight) } + = weight ``` Note: diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 556d71b8b76..9122dc62e39 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -123,7 +123,7 @@ To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa` which translates to `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages and to -`/home/git/.ssh/bitbucket_rsa.pub` for installations from source. +`/home/git/.ssh/bitbucket_rsa` for installations from source. --- @@ -199,7 +199,7 @@ Your GitLab server is now able to connect to Bitbucket over SSH. You should be able to see the "Import projects from Bitbucket" option on the New Project page enabled. -## Acknowledgemts +## Acknowledgements Special thanks to the writer behind the following article: diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index aa666a954bc..79dde620265 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -110,14 +110,14 @@ module SharedIssuable end step 'I sort the list by "Oldest updated"' do - find('button.dropdown-toggle.btn').click + find('button.dropdown-toggle').click page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do click_link "Oldest updated" end end step 'I sort the list by "Least popular"' do - find('button.dropdown-toggle.btn').click + find('button.dropdown-toggle').click page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do click_link 'Least popular' @@ -125,7 +125,7 @@ module SharedIssuable end step 'I sort the list by "Most popular"' do - find('button.dropdown-toggle.btn').click + find('button.dropdown-toggle').click page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do click_link 'Most popular' diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 0d3ddb89dc3..34d9c3c6932 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -128,9 +128,7 @@ module API end def find_project_issue(id) - issue = user_project.issues.find(id) - not_found! unless can?(current_user, :read_issue, issue) - issue + IssuesFinder.new(current_user, project_id: user_project.id).find(id) end def paginate(relation) @@ -198,20 +196,6 @@ module API ActionController::Parameters.new(attrs).permit! end - # Helper method for validating all labels against its names - def validate_label_params(params) - errors = {} - - params[:labels].to_s.split(',').each do |label_name| - label = available_labels.find_or_initialize_by(title: label_name.strip) - next if label.valid? - - errors[label.title] = label.errors - end - - errors - end - # Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601 # format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked. # diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 2fea71870b8..049b4fb214c 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -19,6 +19,15 @@ module API def filter_issues_milestone(issues, milestone) issues.includes(:milestone).where('milestones.title' => milestone) end + + def issue_params + new_params = declared(params, include_parent_namespace: false, include_missing: false).to_h + new_params = new_params.with_indifferent_access + new_params.delete(:id) + new_params.delete(:issue_id) + + new_params + end end resource :issues do @@ -86,6 +95,10 @@ module API end end + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do # Get a list of project issues # @@ -109,7 +122,7 @@ module API # GET /projects/:id/issues?milestone=1.0.0&state=closed # GET /issues?iid=42 get ":id/issues" do - issues = user_project.issues.inc_notes_with_associations.visible_to_user(current_user) + issues = IssuesFinder.new(current_user, project_id: user_project.id).execute.inc_notes_with_associations issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil? @@ -152,17 +165,10 @@ module API post ':id/issues' do required_attributes! [:title] - keys = [:title, :description, :assignee_id, :milestone_id, :due_date, :confidential] + keys = [:title, :description, :assignee_id, :milestone_id, :due_date, :confidential, :labels] keys << :created_at if current_user.admin? || user_project.owner == current_user attrs = attributes_for_keys(keys) - # Validate label names in advance - if (errors = validate_label_params(params)).any? - render_api_error!({ labels: errors }, 400) - end - - attrs[:labels] = params[:labels] if params[:labels] - # Convert and filter out invalid confidential flags attrs['confidential'] = to_boolean(attrs['confidential']) attrs.delete('confidential') if attrs['confidential'].nil? @@ -180,41 +186,35 @@ module API end end - # Update an existing issue - # - # Parameters: - # id (required) - The ID of a project - # issue_id (required) - The ID of a project issue - # title (optional) - 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 - # state_event (optional) - The state event of an issue (close|reopen) - # updated_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 - # Example Request: - # PUT /projects/:id/issues/:issue_id + desc 'Update an existing issue' do + success Entities::Issue + end + params do + requires :id, type: String, desc: 'The ID of a project' + requires :issue_id, type: Integer, desc: "The ID of a project issue" + optional :title, type: String, desc: 'The new title of the issue' + optional :description, type: String, desc: 'The description of an issue' + optional :assignee_id, type: Integer, desc: 'The ID of a user to assign issue' + optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue' + optional :labels, type: String, desc: 'The labels of an issue' + optional :state_event, type: String, values: ['close', 'reopen'], desc: 'The state event of an issue' + # TODO 9.0, use the Grape DateTime type here + optional :updated_at, type: String, desc: 'Date time string, ISO 8601 formatted' + optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY' + # TODO 9.0, use the Grape boolean type here + optional :confidential, type: String, desc: 'Boolean parameter if the issue should be confidential' + end put ':id/issues/:issue_id' do issue = user_project.issues.find(params[:issue_id]) authorize! :update_issue, issue - keys = [:title, :description, :assignee_id, :milestone_id, :state_event, :due_date, :confidential] - keys << :updated_at if current_user.admin? || user_project.owner == current_user - attrs = attributes_for_keys(keys) - - # Validate label names in advance - if (errors = validate_label_params(params)).any? - render_api_error!({ labels: errors }, 400) - end - - attrs[:labels] = params[:labels] if params[:labels] # Convert and filter out invalid confidential flags - attrs['confidential'] = to_boolean(attrs['confidential']) - attrs.delete('confidential') if attrs['confidential'].nil? + params[:confidential] = to_boolean(params[:confidential]) + params.delete(:confidential) if params[:confidential].nil? + + params.delete(:updated_at) unless current_user.admin? || user_project.owner == current_user - issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue) + issue = ::Issues::UpdateService.new(user_project, current_user, issue_params).execute(issue) if issue.valid? present issue, with: Entities::Issue, current_user: current_user, project: user_project diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index e82651a1578..90fa588b455 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -77,11 +77,6 @@ module API mr_params = declared_params - # Validate label names in advance - if (errors = validate_label_params(mr_params)).any? - render_api_error!({ labels: errors }, 400) - end - merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute if merge_request.valid? @@ -157,11 +152,6 @@ module API mr_params = declared_params(include_missing: false) - # Validate label names in advance - if (errors = validate_label_params(mr_params)).any? - render_api_error!({ labels: errors }, 400) - end - merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request) if merge_request.valid? diff --git a/lib/email_template_interceptor.rb b/lib/email_template_interceptor.rb new file mode 100644 index 00000000000..fb04a7824b8 --- /dev/null +++ b/lib/email_template_interceptor.rb @@ -0,0 +1,13 @@ +# Read about interceptors in http://guides.rubyonrails.org/action_mailer_basics.html#intercepting-emails +class EmailTemplateInterceptor + include Gitlab::CurrentSettings + + def self.delivering_email(message) + # Remove HTML part if HTML emails are disabled. + unless current_application_settings.html_emails_enabled + message.part.delete_if do |part| + part.content_type.try(:start_with?, 'text/html') + end + end + end +end diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb index e59d69b72b9..25da8474e95 100644 --- a/lib/gitlab/chat_commands/base_command.rb +++ b/lib/gitlab/chat_commands/base_command.rb @@ -40,9 +40,7 @@ module Gitlab private def find_by_iid(iid) - resource = collection.find_by(iid: iid) - - readable?(resource) ? resource : nil + collection.find_by(iid: iid) end end end diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index 0ec358debc7..b0d3fdbc48a 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -4,6 +4,7 @@ module Gitlab COMMANDS = [ Gitlab::ChatCommands::IssueShow, Gitlab::ChatCommands::IssueCreate, + Gitlab::ChatCommands::IssueSearch, Gitlab::ChatCommands::Deploy, ].freeze diff --git a/lib/gitlab/chat_commands/issue_command.rb b/lib/gitlab/chat_commands/issue_command.rb index f1bc36239d5..84de3e44c70 100644 --- a/lib/gitlab/chat_commands/issue_command.rb +++ b/lib/gitlab/chat_commands/issue_command.rb @@ -6,11 +6,7 @@ module Gitlab end def collection - project.issues - end - - def readable?(issue) - self.class.can?(current_user, :read_issue, issue) + IssuesFinder.new(current_user, project_id: project.id).execute end end end diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb new file mode 100644 index 00000000000..51bf80c800b --- /dev/null +++ b/lib/gitlab/chat_commands/issue_search.rb @@ -0,0 +1,17 @@ +module Gitlab + module ChatCommands + class IssueSearch < IssueCommand + def self.match(text) + /\Aissue\s+search\s+(?<query>.*)/.match(text) + end + + def self.help_message + "issue search <your query>" + end + + def execute(match) + collection.search(match[:query]).limit(QUERY_LIMIT) + end + end + end +end diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 2690938fe82..47d8599e298 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -50,7 +50,7 @@ module Gitlab end def issues - issues = Issue.visible_to_user(current_user).where(project_id: project_ids_relation) + issues = IssuesFinder.new(current_user).execute.where(project_id: project_ids_relation) if query =~ /#(\d+)\z/ issues = issues.where(iid: $1) diff --git a/package.json b/package.json index 350e4cd80c9..961989f8012 100644 --- a/package.json +++ b/package.json @@ -6,13 +6,11 @@ "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html" }, "devDependencies": { - "eslint": "^3.1.1", - "eslint-config-airbnb": "^12.0.0", + "eslint": "^3.10.1", + "eslint-config-airbnb-base": "^10.0.1", "eslint-plugin-filenames": "^1.1.0", - "eslint-plugin-import": "^1.16.0", - "eslint-plugin-jasmine": "^1.8.1", - "eslint-plugin-jsx-a11y": "^2.2.3", - "eslint-plugin-react": "^6.4.1", + "eslint-plugin-import": "^2.2.0", + "eslint-plugin-jasmine": "^2.1.0", "istanbul": "^0.4.5" } } diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index d9a86346c81..0d1545040f1 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -4,7 +4,7 @@ describe AutocompleteController do let!(:project) { create(:project) } let!(:user) { create(:user) } - context 'users and members' do + context 'GET users' do let!(:user2) { create(:user) } let!(:non_member) { create(:user) } @@ -180,7 +180,7 @@ describe AutocompleteController do end end - context 'projects' do + context 'GET projects' do let(:authorized_project) { create(:project) } let(:authorized_search_project) { create(:project, name: 'rugged') } diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 52d13fb6f9e..3efef757ae2 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -36,4 +36,53 @@ describe Projects::BlobController do end end end + + describe 'PUT update' do + let(:default_params) do + { + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: 'master/CHANGELOG', + target_branch: 'master', + content: 'Added changes', + commit_message: 'Update CHANGELOG' + } + end + + def blob_after_edit_path + namespace_project_blob_path(project.namespace, project, 'master/CHANGELOG') + end + + it 'redirects to blob' do + put :update, default_params + + expect(response).to redirect_to(blob_after_edit_path) + end + + context '?from_merge_request_iid' do + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let(:mr_params) { default_params.merge(from_merge_request_iid: merge_request.iid) } + + it 'redirects to MR diff' do + put :update, mr_params + + after_edit_path = diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) + file_anchor = "##{Digest::SHA1.hexdigest('CHANGELOG')}" + expect(response).to redirect_to(after_edit_path + file_anchor) + end + + context "when user doesn't have access" do + before do + other_project = create(:empty_project) + merge_request.update!(source_project: other_project, target_project: other_project) + end + + it "it redirect to blob" do + put :update, mr_params + + expect(response).to redirect_to(blob_after_edit_path) + end + end + end + end end diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index f7cf006efd6..b88586b8678 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -94,6 +94,24 @@ describe Projects::BranchesController do branch_name: branch, issue_iid: issue.iid end + + context 'without issue feature access' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE) + project.team.truncate + end + + it "doesn't post a system note" do + expect(SystemNoteService).not_to receive(:new_issue_branch) + + post :create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + branch_name: branch, + issue_iid: issue.iid + end + end end end diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb index 936320a3709..193a3f6b5a3 100644 --- a/spec/controllers/projects/todo_controller_spec.rb +++ b/spec/controllers/projects/todo_controller_spec.rb @@ -4,7 +4,7 @@ describe Projects::TodosController do include ApiHelpers let(:user) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:issue) { create(:issue, project: project) } let(:merge_request) { create(:merge_request, source_project: project) } @@ -42,7 +42,7 @@ describe Projects::TodosController do end end - context 'when not authorized' do + context 'when not authorized for project' do it 'does not create todo for issue that user has no access to' do sign_in(user) expect do @@ -60,6 +60,19 @@ describe Projects::TodosController do expect(response).to have_http_status(302) end end + + context 'when not authorized for issue' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE) + sign_in(user) + end + + it "doesn't create todo" do + expect{ go }.not_to change { user.todos.count } + expect(response).to have_http_status(404) + end + end end end diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 2d762fdaa04..d76fe9f580f 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -3,6 +3,28 @@ require 'spec_helper' describe SnippetsController do let(:user) { create(:user) } + describe 'GET #new' do + context 'when signed in' do + before do + sign_in(user) + end + + it 'responds with status 200' do + get :new + + expect(response).to have_http_status(200) + end + end + + context 'when not signed in' do + it 'redirects to the sign in page' do + get :new + + expect(response).to redirect_to(new_user_session_path) + end + end + end + describe 'GET #show' do context 'when the personal snippet is private' do let(:personal_snippet) { create(:personal_snippet, :private, author: user) } diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb index d14a1158b67..a6b841c0210 100644 --- a/spec/features/profiles/preferences_spec.rb +++ b/spec/features/profiles/preferences_spec.rb @@ -73,7 +73,7 @@ describe 'Profile > Preferences', feature: true do expect(page.current_path).to eq starred_dashboard_projects_path end - click_link 'Your Projects' + click_link 'Your projects' expect(page).not_to have_content("You don't have starred projects yet") expect(page.current_path).to eq dashboard_projects_path diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb new file mode 100644 index 00000000000..a820d07ab3b --- /dev/null +++ b/spec/features/projects/blobs/edit_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +feature 'Editing file blob', feature: true, js: true do + include WaitForAjax + + given(:user) { create(:user) } + given(:role) { :developer } + given(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') } + given(:project) { merge_request.target_project } + + background do + login_as(user) + project.team << [user, role] + end + + def edit_and_commit + wait_for_ajax + first('.file-actions').click_link 'Edit' + execute_script('ace.edit("editor").setValue("class NextFeature\nend\n")') + click_button 'Commit Changes' + end + + context 'from MR diff' do + before do + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) + edit_and_commit + end + + scenario 'returns me to the mr' do + expect(page).to have_content(merge_request.title) + end + end + + context 'from blob file path' do + before do + visit namespace_project_blob_path(project.namespace, project, '/feature/files/ruby/feature.rb') + edit_and_commit + end + + scenario 'updates content' do + expect(page).to have_content 'successfully committed' + expect(page).to have_content 'NextFeature' + end + end +end diff --git a/spec/features/snippets/create_snippet_spec.rb b/spec/features/snippets/create_snippet_spec.rb new file mode 100644 index 00000000000..cb95e7828db --- /dev/null +++ b/spec/features/snippets/create_snippet_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' + +feature 'Create Snippet', feature: true do + before do + login_as :user + visit new_snippet_path + end + + scenario 'Authenticated user creates a snippet' do + fill_in 'personal_snippet_title', with: 'My Snippet Title' + page.within('.file-editor') do + find(:xpath, "//input[@id='personal_snippet_content']").set 'Hello World!' + end + + click_button 'Create snippet' + + expect(page).to have_content('My Snippet Title') + expect(page).to have_content('Hello World!') + end +end diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb index d7880d5778f..ff30ffd7820 100644 --- a/spec/features/variables_spec.rb +++ b/spec/features/variables_spec.rb @@ -29,6 +29,31 @@ describe 'Project variables', js: true do end end + it 'reveals and hides new variable' do + fill_in('variable_key', with: 'key') + fill_in('variable_value', with: 'key value') + click_button('Add new variable') + + page.within('.variables-table') do + expect(page).to have_content('key') + expect(page).to have_content('******') + end + + click_button('Reveal Values') + + page.within('.variables-table') do + expect(page).to have_content('key') + expect(page).to have_content('key value') + end + + click_button('Hide Values') + + page.within('.variables-table') do + expect(page).to have_content('key') + expect(page).to have_content('******') + end + end + it 'deletes variable' do page.within('.variables-table') do find('.btn-variable-delete').click diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index d694727880f..3983cad4c13 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -109,7 +109,7 @@ describe('Build', () => { expect($.ajax.calls.count()).toBe(2); let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1); expect(url).toBe( - `${BUILD_URL}/trace.json?state=${encodeURIComponent(INITIAL_BUILD_TRACE_STATE)}` + `${BUILD_URL}/trace.json?state=${encodeURIComponent(INITIAL_BUILD_TRACE_STATE)}`, ); expect(dataType).toBe('json'); expect(success).toEqual(jasmine.any(Function)); diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 index c9ac7a73fd0..76e81233e89 100644 --- a/spec/javascripts/environments/environment_actions_spec.js.es6 +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -28,10 +28,10 @@ describe('Actions Component', () => { }); expect( - component.$el.querySelectorAll('.dropdown-menu li').length + component.$el.querySelectorAll('.dropdown-menu li').length, ).toEqual(actionsMock.length); expect( - component.$el.querySelector('.dropdown-menu li a').getAttribute('href') + component.$el.querySelector('.dropdown-menu li a').getAttribute('href'), ).toEqual(actionsMock[0].play_path); }); }); diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index 3c15e3b7719..14e90a9dd1b 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -141,18 +141,18 @@ describe('Environment item', () => { describe('With deployment', () => { it('should render deployment internal id', () => { expect( - component.$el.querySelector('.deployment-column span').textContent + component.$el.querySelector('.deployment-column span').textContent, ).toContain(environment.last_deployment.iid); expect( - component.$el.querySelector('.deployment-column span').textContent + component.$el.querySelector('.deployment-column span').textContent, ).toContain('#'); }); describe('With user information', () => { it('should render user avatar with link to profile', () => { expect( - component.$el.querySelector('.js-deploy-user-container').getAttribute('href') + component.$el.querySelector('.js-deploy-user-container').getAttribute('href'), ).toEqual(environment.last_deployment.user.web_url); }); }); @@ -160,13 +160,13 @@ describe('Environment item', () => { describe('With build url', () => { it('Should link to build url provided', () => { expect( - component.$el.querySelector('.build-link').getAttribute('href') + component.$el.querySelector('.build-link').getAttribute('href'), ).toEqual(environment.last_deployment.deployable.build_path); }); it('Should render deployable name and id', () => { expect( - component.$el.querySelector('.build-link').getAttribute('href') + component.$el.querySelector('.build-link').getAttribute('href'), ).toEqual(environment.last_deployment.deployable.build_path); }); }); @@ -174,7 +174,7 @@ describe('Environment item', () => { describe('With commit information', () => { it('should render commit component', () => { expect( - component.$el.querySelector('.js-commit-component') + component.$el.querySelector('.js-commit-component'), ).toBeDefined(); }); }); @@ -183,7 +183,7 @@ describe('Environment item', () => { describe('With manual actions', () => { it('Should render actions component', () => { expect( - component.$el.querySelector('.js-manual-actions-container') + component.$el.querySelector('.js-manual-actions-container'), ).toBeDefined(); }); }); @@ -191,7 +191,7 @@ describe('Environment item', () => { describe('With external URL', () => { it('should render external url component', () => { expect( - component.$el.querySelector('.js-external-url-container') + component.$el.querySelector('.js-external-url-container'), ).toBeDefined(); }); }); @@ -199,7 +199,7 @@ describe('Environment item', () => { describe('With stop action', () => { it('Should render stop action component', () => { expect( - component.$el.querySelector('.js-stop-component-container') + component.$el.querySelector('.js-stop-component-container'), ).toBeDefined(); }); }); @@ -207,7 +207,7 @@ describe('Environment item', () => { describe('With retry action', () => { it('Should render rollback component', () => { expect( - component.$el.querySelector('.js-rollback-component-container') + component.$el.querySelector('.js-rollback-component-container'), ).toBeDefined(); }); }); diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index 9b0b3cb1c65..17c00acf63e 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -5,11 +5,11 @@ //= require ./mock_data (() => { - beforeEach(() => { - gl.environmentsList.EnvironmentsStore.create(); - }); - describe('Store', () => { + beforeEach(() => { + gl.environmentsList.EnvironmentsStore.create(); + }); + it('should start with a blank state', () => { expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(0); expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(0); diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js.es6 index 651d1f0f975..ed6166a25a8 100644 --- a/spec/javascripts/smart_interval_spec.js.es6 +++ b/spec/javascripts/smart_interval_spec.js.es6 @@ -37,7 +37,7 @@ const intervalConfig = this.smartInterval.cfg; const iterationCount = 4; const maxIntervalAfterIterations = intervalConfig.startingInterval * - Math.pow(intervalConfig.incrementByFactorOf, (iterationCount - 1)); // 40 + (intervalConfig.incrementByFactorOf ** (iterationCount - 1)); // 40 const currentInterval = interval.getCurrentInterval(); // Provide some flexibility for performance of testing environment diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6 index 0e3b82967c1..b1dbc8bd5fa 100644 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ b/spec/javascripts/vue_common_components/commit_spec.js.es6 @@ -74,26 +74,26 @@ describe('Commit component', () => { describe('Given commit title and author props', () => { it('Should render a link to the author profile', () => { expect( - component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href') + component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'), ).toEqual(props.author.web_url); }); it('Should render the author avatar with title and alt attributes', () => { expect( - component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('title') + component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('title'), ).toContain(props.author.username); expect( - component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt') + component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt'), ).toContain(`${props.author.username}'s avatar`); }); }); it('should render the commit title', () => { expect( - component.$el.querySelector('a.commit-row-message').getAttribute('href') + component.$el.querySelector('a.commit-row-message').getAttribute('href'), ).toEqual(props.commit_url); expect( - component.$el.querySelector('a.commit-row-message').textContent + component.$el.querySelector('a.commit-row-message').textContent, ).toContain(props.title); }); }); @@ -119,7 +119,7 @@ describe('Commit component', () => { }); expect( - component.$el.querySelector('.commit-title span').textContent + component.$el.querySelector('.commit-title span').textContent, ).toContain('Cant find HEAD commit for this branch'); }); }); diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb new file mode 100644 index 00000000000..24c06a967fa --- /dev/null +++ b/spec/lib/gitlab/chat_commands/issue_search_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::IssueSearch, service: true do + describe '#execute' do + let!(:issue) { create(:issue, title: 'find me') } + let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') } + let(:project) { issue.project } + let(:user) { issue.author } + let(:regex_match) { described_class.match("issue search find") } + + subject do + described_class.new(project, user).execute(regex_match) + end + + context 'when the user has no access' do + it 'only returns the open issues' do + expect(subject).not_to include(confidential) + end + end + + context 'the user has access' do + before do + project.team << [user, :master] + end + + it 'returns all results' do + expect(subject).to include(confidential, issue) + end + end + + context 'without hits on the query' do + it 'returns an empty collection' do + expect(subject).to be_empty + end + end + end + + describe 'self.match' do + let(:query) { "my search keywords" } + it 'matches the query' do + match = described_class.match("issue search #{query}") + + expect(match[:query]).to eq(query) + end + end +end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index a0fdad87eee..3cd9863ec6a 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -65,6 +65,14 @@ describe Gitlab::ProjectSearchResults, lib: true do end end + it 'does not list issues on private projects' do + issue = create(:issue, project: project) + + results = described_class.new(user, project, issue.title) + + expect(results.objects('issues')).not_to include issue + end + describe 'confidential issues' do let(:query) { 'issue' } let(:author) { create(:user) } @@ -72,6 +80,7 @@ describe Gitlab::ProjectSearchResults, lib: true do let(:non_member) { create(:user) } let(:member) { create(:user) } let(:admin) { create(:admin) } + let(:project) { create(:empty_project, :internal) } let!(:issue) { create(:issue, project: project, title: 'Issue 1') } let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) } let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) } diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index dfbefad6367..f23e3522625 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -12,35 +12,48 @@ describe Gitlab::SearchResults do let!(:milestone) { create(:milestone, project: project, title: 'foo') } let(:results) { described_class.new(user, Project.all, 'foo') } - describe '#projects_count' do - it 'returns the total amount of projects' do - expect(results.projects_count).to eq(1) + context 'as a user with access' do + before do + project.team << [user, :developer] end - end - describe '#issues_count' do - it 'returns the total amount of issues' do - expect(results.issues_count).to eq(1) + describe '#projects_count' do + it 'returns the total amount of projects' do + expect(results.projects_count).to eq(1) + end end - end - describe '#merge_requests_count' do - it 'returns the total amount of merge requests' do - expect(results.merge_requests_count).to eq(1) + describe '#issues_count' do + it 'returns the total amount of issues' do + expect(results.issues_count).to eq(1) + end + end + + describe '#merge_requests_count' do + it 'returns the total amount of merge requests' do + expect(results.merge_requests_count).to eq(1) + end end - end - describe '#milestones_count' do - it 'returns the total amount of milestones' do - expect(results.milestones_count).to eq(1) + describe '#milestones_count' do + it 'returns the total amount of milestones' do + expect(results.milestones_count).to eq(1) + end end end + it 'does not list issues on private projects' do + private_project = create(:empty_project, :private) + issue = create(:issue, project: private_project, title: 'foo') + + expect(results.objects('issues')).not_to include issue + end + describe 'confidential issues' do - let(:project_1) { create(:empty_project) } - let(:project_2) { create(:empty_project) } - let(:project_3) { create(:empty_project) } - let(:project_4) { create(:empty_project) } + let(:project_1) { create(:empty_project, :internal) } + let(:project_2) { create(:empty_project, :internal) } + let(:project_3) { create(:empty_project, :internal) } + let(:project_4) { create(:empty_project, :internal) } let(:query) { 'issue' } let(:limit_projects) { Project.where(id: [project_1.id, project_2.id, project_3.id]) } let(:author) { create(:user) } diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 932a5dc4862..b692142713f 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -50,7 +50,7 @@ describe Notify do context 'when enabled email_author_in_body' do before do - allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true) + stub_application_setting(email_author_in_body: true) end it 'contains a link to note author' do @@ -229,7 +229,7 @@ describe Notify do context 'when enabled email_author_in_body' do before do - allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true) + stub_application_setting(email_author_in_body: true) end it 'contains a link to note author' do @@ -607,7 +607,7 @@ describe Notify do context 'when enabled email_author_in_body' do before do - allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true) + stub_application_setting(email_author_in_body: true) end it 'contains a link to note author' do @@ -686,6 +686,79 @@ describe Notify do end end end + + context 'items that are noteable, emails for a note on a diff' do + let(:note_author) { create(:user, name: 'author_name') } + + before :each do + allow(Note).to receive(:find).with(note.id).and_return(note) + end + + shared_examples 'a note email on a diff' do |model| + let(:note) { create(model, project: project, author: note_author) } + + it "includes diffs with character-level highlighting" do + is_expected.to have_body_text /<span class=\"p\">}<\/span><\/span>/ + end + + it 'contains a link to the diff file' do + is_expected.to have_body_text /#{note.diff_file.file_path}/ + end + + it_behaves_like 'it should have Gmail Actions links' + + it 'is sent as the author' do + sender = subject.header[:from].addrs[0] + expect(sender.display_name).to eq(note_author.name) + expect(sender.address).to eq(gitlab_sender) + end + + it 'is sent to the given recipient' do + is_expected.to deliver_to recipient.notification_email + end + + it 'contains the message from the note' do + is_expected.to have_body_text /#{note.note}/ + end + + it 'does not contain note author' do + is_expected.not_to have_body_text /wrote\:/ + end + + context 'when enabled email_author_in_body' do + before do + stub_application_setting(email_author_in_body: true) + end + + it 'contains a link to note author' do + is_expected.to have_body_text note.author_name + is_expected.to have_body_text /wrote\:/ + end + end + end + + describe 'on a commit' do + let(:commit) { project.commit } + let(:note) { create(:diff_note_on_commit) } + + subject { Notify.note_commit_email(recipient.id, note.id) } + + it_behaves_like 'a note email on a diff', :diff_note_on_commit + it_behaves_like 'it should show Gmail Actions View Commit link' + it_behaves_like 'a user cannot unsubscribe through footer link' + end + + describe 'on a merge request' do + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let(:note) { create(:diff_note_on_merge_request) } + + subject { Notify.note_merge_request_email(recipient.id, note.id) } + + it_behaves_like 'a note email on a diff', :diff_note_on_merge_request + it_behaves_like 'it should show Gmail Actions View Merge request link' + it_behaves_like 'an unsubscribeable thread' + end + end end context 'for a group' do @@ -1099,4 +1172,38 @@ describe Notify do is_expected.to have_body_text /#{diff_path}/ end end + + describe 'HTML emails setting' do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:multipart_mail) { Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab") } + + context 'when disabled' do + it 'only sends the text template' do + stub_application_setting(html_emails_enabled: false) + + EmailTemplateInterceptor.delivering_email(multipart_mail) + + expect(multipart_mail).to have_part_with('text/plain') + expect(multipart_mail).not_to have_part_with('text/html') + end + end + + context 'when enabled' do + it 'sends a multipart message' do + stub_application_setting(html_emails_enabled: true) + + EmailTemplateInterceptor.delivering_email(multipart_mail) + + expect(multipart_mail).to have_part_with('text/plain') + expect(multipart_mail).to have_part_with('text/html') + end + end + + matcher :have_part_with do |expected| + match do |actual| + actual.body.parts.any? { |part| part.content_type.try(:match, %r(#{expected})) } + end + end + end end diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb index 7691d690db0..7771785ead3 100644 --- a/spec/models/cycle_analytics/code_spec.rb +++ b/spec/models/cycle_analytics/code_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#code', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } context 'with deployment' do generate_cycle_analytics_spec( diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb index f649b44d367..5ed3d37f2fb 100644 --- a/spec/models/cycle_analytics/issue_spec.rb +++ b/spec/models/cycle_analytics/issue_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#issue', models: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } generate_cycle_analytics_spec( phase: :issue, diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb index 2cdefbeef21..baf3e3241a1 100644 --- a/spec/models/cycle_analytics/plan_spec.rb +++ b/spec/models/cycle_analytics/plan_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#plan', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } generate_cycle_analytics_spec( phase: :plan, diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb index 1f5e5cab92d..21b9c6e7150 100644 --- a/spec/models/cycle_analytics/production_spec.rb +++ b/spec/models/cycle_analytics/production_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#production', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } generate_cycle_analytics_spec( phase: :production, diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb index 0ed080a42b1..158621d59a4 100644 --- a/spec/models/cycle_analytics/review_spec.rb +++ b/spec/models/cycle_analytics/review_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#review', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } generate_cycle_analytics_spec( phase: :review, diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb index af1c4477ddb..dad653964b7 100644 --- a/spec/models/cycle_analytics/staging_spec.rb +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#staging', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } generate_cycle_analytics_spec( phase: :staging, diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/models/cycle_analytics/summary_spec.rb index 9d67bc82cba..725bc68b25f 100644 --- a/spec/models/cycle_analytics/summary_spec.rb +++ b/spec/models/cycle_analytics/summary_spec.rb @@ -4,7 +4,7 @@ describe CycleAnalytics::Summary, models: true do let(:project) { create(:project) } let(:from) { Time.now } let(:user) { create(:user, :admin) } - subject { described_class.new(project, from: from) } + subject { described_class.new(project, user, from: from) } describe "#new_issues" do it "finds the number of issues created after the 'from date'" do diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb index 02ddfeed9c1..2313724e8f3 100644 --- a/spec/models/cycle_analytics/test_spec.rb +++ b/spec/models/cycle_analytics/test_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#test', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, from: from_date) } + subject { CycleAnalytics.new(project, user, from: from_date) } generate_cycle_analytics_spec( phase: :test, diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb index 0142706d140..2a67c60b978 100644 --- a/spec/models/discussion_spec.rb +++ b/spec/models/discussion_spec.rb @@ -590,4 +590,23 @@ describe Discussion, model: true do end end end + + describe "#truncated_diff_lines" do + let(:truncated_lines) { subject.truncated_diff_lines } + + context "when diff is greater than allowed number of truncated diff lines " do + it "returns fewer lines" do + expect(subject.diff_lines.count).to be > described_class::NUMBER_OF_TRUNCATED_DIFF_LINES + + expect(truncated_lines.count).to be <= described_class::NUMBER_OF_TRUNCATED_DIFF_LINES + end + end + + context "when some diff lines are meta" do + it "returns no meta lines" do + expect(subject.diff_lines).to include(be_meta) + expect(truncated_lines).not_to include(be_meta) + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index da38254d1bc..8abcce42ce0 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -361,10 +361,15 @@ describe Project, models: true do describe '#get_issue' do let(:project) { create(:empty_project) } let!(:issue) { create(:issue, project: project) } + let(:user) { create(:user) } + + before do + project.team << [user, :developer] + end context 'with default issues tracker' do it 'returns an issue' do - expect(project.get_issue(issue.iid)).to eq issue + expect(project.get_issue(issue.iid, user)).to eq issue end it 'returns count of open issues' do @@ -372,7 +377,12 @@ describe Project, models: true do end it 'returns nil when no issue found' do - expect(project.get_issue(999)).to be_nil + expect(project.get_issue(999, user)).to be_nil + end + + it "returns nil when user doesn't have access" do + user = create(:user) + expect(project.get_issue(issue.iid, user)).to eq nil end end @@ -382,7 +392,7 @@ describe Project, models: true do end it 'returns an ExternalIssue' do - issue = project.get_issue('FOO-1234') + issue = project.get_issue('FOO-1234', user) expect(issue).to be_kind_of(ExternalIssue) expect(issue.iid).to eq 'FOO-1234' expect(issue.project).to eq project diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 7bae055b241..ae7994af981 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -365,6 +365,24 @@ describe API::API, api: true do let(:base_url) { "/projects/#{project.id}" } let(:title) { milestone.title } + it "returns 404 on private projects for other users" do + private_project = create(:empty_project, :private) + create(:issue, project: private_project) + + get api("/projects/#{private_project.id}/issues", non_member) + + expect(response).to have_http_status(404) + end + + it 'returns no issues when user has access to project but not issues' do + restricted_project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + create(:issue, project: restricted_project) + + get api("/projects/#{restricted_project.id}/issues", non_member) + + expect(json_response).to eq([]) + end + it 'returns project issues without confidential issues for non project members' do get api("#{base_url}/issues", non_member) expect(response).to have_http_status(200) @@ -697,6 +715,14 @@ describe API::API, api: true do expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) end end + + context 'the user can only read the issue' do + it 'cannot create new labels' do + expect do + post api("/projects/#{project.id}/issues", non_member), title: 'new issue', labels: 'label, label2' + end.not_to change { project.labels.count } + end + end end describe 'POST /projects/:id/issues with spam filtering' do @@ -839,8 +865,8 @@ describe API::API, api: true do end it 'removes all labels' do - put api("/projects/#{project.id}/issues/#{issue.id}", user), - labels: '' + put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' + expect(response).to have_http_status(200) expect(json_response['labels']).to eq([]) end @@ -892,8 +918,8 @@ describe API::API, api: true do update_time = 2.weeks.ago put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: 'label3', state_event: 'close', updated_at: update_time - expect(response).to have_http_status(200) + expect(response).to have_http_status(200) expect(json_response['labels']).to include 'label3' expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time) end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 37fcb2bc3a9..3ecf3eea5f5 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -402,14 +402,6 @@ describe API::API, api: true do end end - describe "PUT /projects/:id/merge_requests/:merge_request_id to close MR" do - it "returns merge_request" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" - expect(response).to have_http_status(200) - expect(json_response['state']).to eq('closed') - end - end - describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do let(:pipeline) { create(:ci_pipeline_without_jobs) } @@ -486,6 +478,15 @@ describe API::API, api: true do end describe "PUT /projects/:id/merge_requests/:merge_request_id" do + context "to close a MR" do + it "returns merge_request" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" + + expect(response).to have_http_status(200) + expect(json_response['state']).to eq('closed') + end + end + it "updates title and returns merge_request" do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title" expect(response).to have_http_status(200) @@ -511,10 +512,10 @@ describe API::API, api: true do end it 'allows special label names' do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", - user), - title: 'new issue', - labels: 'label, label?, label&foo, ?, &' + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), + title: 'new issue', + labels: 'label, label?, label&foo, ?, &' + expect(response.status).to eq(200) expect(json_response['labels']).to include 'label' expect(json_response['labels']).to include 'label?' @@ -543,7 +544,7 @@ describe API::API, api: true do it "returns 404 if note is attached to non existent merge request" do post api("/projects/#{project.id}/merge_requests/404/comments", user), - note: 'My comment' + note: 'My comment' expect(response).to have_http_status(404) end end diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index ce9c96ace21..bb0344e5995 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -128,7 +128,7 @@ describe API::API, api: true do ) end - it 'retusn status 200' do + it 'returns status 200' do post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger"), params expect(response).to have_http_status(200) diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb index 6dcfaec259e..60c9642ee2c 100644 --- a/spec/serializers/build_entity_spec.rb +++ b/spec/serializers/build_entity_spec.rb @@ -1,23 +1,30 @@ require 'spec_helper' describe BuildEntity do + let(:build) { create(:ci_build) } + let(:entity) do described_class.new(build, request: double) end subject { entity.as_json } - context 'when build is a regular job' do - let(:build) { create(:ci_build) } + it 'contains paths to build page and retry action' do + expect(subject).to include(:build_path, :retry_path) + end - it 'contains paths to build page and retry action' do - expect(subject).to include(:build_path, :retry_path) - expect(subject).not_to include(:play_path) - end + it 'does not contain sensitive information' do + expect(subject).not_to include(/token/) + expect(subject).not_to include(/variables/) + end + + it 'contains timestamps' do + expect(subject).to include(:created_at, :updated_at) + end - it 'does not contain sensitive information' do - expect(subject).not_to include(/token/) - expect(subject).not_to include(/variables/) + context 'when build is a regular job' do + it 'does not contain path to play action' do + expect(subject).not_to include(:play_path) end end diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb index ddf3527dc0f..13654a0881c 100644 --- a/spec/services/labels/transfer_service_spec.rb +++ b/spec/services/labels/transfer_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Labels::TransferService, services: true do describe '#execute' do - let(:user) { create(:user) } + let(:user) { create(:admin) } let(:group_1) { create(:group) } let(:group_2) { create(:group) } let(:group_3) { create(:group) } diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 3f5df049ea2..dc945ca4868 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -24,6 +24,8 @@ describe MergeRequests::BuildService, services: true do end before do + project.team << [user, :guest] + allow(CompareService).to receive_message_chain(:new, :execute).and_return(compare) allow(project).to receive(:commit).and_return(commit_1) allow(project).to receive(:commit).and_return(commit_2) @@ -168,6 +170,16 @@ describe MergeRequests::BuildService, services: true do expect(merge_request.title).to eq("Resolve \"#{issue.title}\"") end + context 'when issue is not accessible to user' do + before do + project.team.truncate + end + + it 'uses branch title as the merge request title' do + expect(merge_request.title).to eq("#{issue.iid} fix issue") + end + end + context 'issue does not exist' do let(:source_branch) { "#{issue.iid.succ}-fix-issue" } |