diff options
author | Lin Jen-Shin <godfat@godfat.org> | 2016-10-24 23:56:09 +0800 |
---|---|---|
committer | Lin Jen-Shin <godfat@godfat.org> | 2016-10-24 23:56:09 +0800 |
commit | 517effe9f03ccd7b70ce7092b3295c9734060b39 (patch) | |
tree | 2746593c794021582e114afb797a4f902a75d4bc /app | |
parent | 68f964ad14a007a99df3d8d7f76d125fbe7f67c9 (diff) | |
parent | 4028022f56f81f9cbe1227dae53c878f702bd8fa (diff) | |
download | gitlab-ce-517effe9f03ccd7b70ce7092b3295c9734060b39.tar.gz |
Merge remote-tracking branch 'upstream/master' into show-commit-status-from-source-project
* upstream/master: (368 commits)
Use root_url for issue boards user link
removes extra line for empty milestone description
code formatting corrected
Fix reply-by-email not working due to queue name mismatch
Remove duplicate code in repository cache clearing
Expire and build repository cache after project import
refactor(email): use setter method instead AR callbacks
Move spec/mailers/shared/notify.rb to spec/support
Fix status code expectation
Stop clearing the database cache on rake cache:clear
Fix error in generating labels
Fix bug where e-mails were not being sent out via Sidekiq
Document link syntax introduced by !5586
Fix documents and comments on Build API `scope`. #23146 #19131
adds entry in CHANGELOG
removes extra line for empty issue description
Re-organize queues to use for Sidekiq
Fix wrong endpoint in api/users documentation, fix same typo in spec describe blocks
Update CHANGELOG
Fix object data to be sent to fetch analytics data
...
Diffstat (limited to 'app')
227 files changed, 2522 insertions, 865 deletions
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif Binary files differnew file mode 100644 index 00000000000..3f4ef31947b --- /dev/null +++ b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif Binary files differnew file mode 100644 index 00000000000..387628f831c --- /dev/null +++ b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gif Binary files differnew file mode 100644 index 00000000000..5f8f8ca143c --- /dev/null +++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gif diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif Binary files differnew file mode 100644 index 00000000000..27a55b1d61f --- /dev/null +++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif Binary files differnew file mode 100644 index 00000000000..8fe3281d2f6 --- /dev/null +++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif Binary files differnew file mode 100644 index 00000000000..4260e312929 --- /dev/null +++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gif Binary files differnew file mode 100644 index 00000000000..6de166ce0a2 --- /dev/null +++ b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gif diff --git a/app/assets/javascripts/cycle_analytics.js.es6 b/app/assets/javascripts/cycle_analytics.js.es6 index cd9886ba58d..20791bab942 100644 --- a/app/assets/javascripts/cycle_analytics.js.es6 +++ b/app/assets/javascripts/cycle_analytics.js.es6 @@ -1,3 +1,5 @@ +//= require vue + ((global) => { const COOKIE_NAME = 'cycle_analytics_help_dismissed'; @@ -34,7 +36,11 @@ method: 'GET', dataType: 'json', contentType: 'application/json', - data: { start_date: options.startDate } + data: { + cycle_analytics: { + start_date: options.startDate + } + } }).done((data) => { this.decorateData(data); this.initDropdown(); diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 73691f40c74..afc0d6f8c62 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -168,6 +168,8 @@ shortcut_handler = new ShortcutsNavigation(); new ShortcutsBlob(true); break; + case 'groups:labels:new': + case 'groups:labels:edit': case 'projects:labels:new': case 'projects:labels:edit': new Labels(); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index f1e719937c7..b4f6e70f694 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -266,7 +266,7 @@ }, fieldName: $dropdown.data('field-name'), id: function(label) { - if (label.id <= 0) return; + if (label.id <= 0) return label.title; if ($dropdown.hasClass('js-issuable-form-dropdown')) { return label.id; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index fd21aa1fefa..9f28738e06b 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -388,28 +388,25 @@ // So we dont affix the tabs on these if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; - var tabsWidth = $tabs.outerWidth(), - $diffTabs = $('#diff-notes-app'), - offsetTop = $tabs.offset().top - ($('.navbar-fixed-top').height() + $('.layout-nav').height()); + var $diffTabs = $('#diff-notes-app'), + $fixedNav = $('.navbar-fixed-top'), + $layoutNav = $('.layout-nav'); $tabs.off('affix.bs.affix affix-top.bs.affix') .affix({ offset: { - top: offsetTop + top: function () { + var tabsTop = $diffTabs.offset().top - $tabs.height(); + tabsTop = tabsTop - ($fixedNav.height() + $layoutNav.height()); + + return tabsTop; + } } }).on('affix.bs.affix', function () { - $tabs.css({ - left: $tabs.offset().left, - width: tabsWidth - }); $diffTabs.css({ marginTop: $tabs.height() }); }).on('affix-top.bs.affix', function () { - $tabs.css({ - left: '', - width: '' - }); $diffTabs.css({ marginTop: '' }); diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index fcadc4bc515..3ff6851d59b 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -17,6 +17,12 @@ View on <%- external_url_formatted %> </a> </span> + <span class="stop-env-container js-stop-env-link"> + <a href="<%- stop_url %>" class="close-evn-link" data-method="post" rel="nofollow" data-confirm="Are you sure you want to stop this environment?"> + <i class="fa fa-stop-circle-o"/> + Stop environment + </a> + </span> </div> </div>`; @@ -205,6 +211,11 @@ if ($(`.mr-state-widget #${ environment.id }`).length) return; const $template = $(DEPLOYMENT_TEMPLATE); if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove(); + + if (!environment.stop_url) { + $('.js-stop-env-link', $template).remove(); + } + if (environment.deployed_at && environment.deployed_at_formatted) { environment.deployed_at = $.timeago(environment.deployed_at) + '.'; } else { diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js index 3cf41505814..478e82aa14d 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/project_new.js @@ -4,9 +4,8 @@ this.ProjectNew = (function() { function ProjectNew() { this.toggleSettings = bind(this.toggleSettings, this); - this.$selects = $('.features select').filter(function () { - return $(this).data('field'); - }); + this.$selects = $('.features select'); + this.$repoSelects = this.$selects.filter('.js-repo-select'); $('.project-edit-container').on('ajax:before', (function(_this) { return function() { @@ -16,6 +15,7 @@ })(this)); this.toggleSettings(); this.toggleSettingsOnclick(); + this.toggleRepoVisibility(); } ProjectNew.prototype.toggleSettings = function() { @@ -43,6 +43,38 @@ } }; + ProjectNew.prototype.toggleRepoVisibility = function () { + var $repoAccessLevel = $('.js-repo-access-level select'); + + this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']") + .nextAll() + .hide(); + + $repoAccessLevel.off('change') + .on('change', function () { + var selectedVal = parseInt($repoAccessLevel.val()); + + this.$repoSelects.each(function () { + var $this = $(this), + repoSelectVal = parseInt($this.val()); + + $this.find('option').show(); + + if (selectedVal < repoSelectVal) { + $this.val(selectedVal); + } + + $this.find("option[value='" + selectedVal + "']").nextAll().hide(); + }); + + if (selectedVal) { + this.$repoSelects.removeClass('disabled'); + } else { + this.$repoSelects.addClass('disabled'); + } + }.bind(this)); + }; + return ProjectNew; })(); diff --git a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 index 7aeb5f92514..7aeb5f92514 100644 --- a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 index 46beca469b9..46beca469b9 100644 --- a/app/assets/javascripts/protected_branch_create.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 diff --git a/app/assets/javascripts/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 index 983322cbecc..983322cbecc 100644 --- a/app/assets/javascripts/protected_branch_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 index 15a6dca2875..15a6dca2875 100644 --- a/app/assets/javascripts/protected_branch_edit.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 diff --git a/app/assets/javascripts/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 index 9ff0fd12c76..9ff0fd12c76 100644 --- a/app/assets/javascripts/protected_branch_edit_list.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js new file mode 100644 index 00000000000..15b3affd469 --- /dev/null +++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js @@ -0,0 +1 @@ +/*= require_tree . */ diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 81e4e264560..800e2dba018 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -372,3 +372,5 @@ table { margin-right: -$gl-padding; border-top: 1px solid $border-color; } + +.hide-bottom-border { border-bottom: none !important; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 13c1bbf0359..f49d7b92a00 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -167,7 +167,6 @@ */ &.code { padding: 0; - -webkit-overflow-scrolling: auto; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987 } } } diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 8bb047db2dd..7baa4296abf 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -27,3 +27,15 @@ body { .container-limited { max-width: $fixed-layout-width; } + + +/* The following prevents side effects related to iOS Safari's implementation of -webkit-overflow-scrolling: touch, +which is applied to the body by jquery.nicescroling plugin to force hardware acceleration for momentum scrolling. Side +effects are commonly related to inconsisent z-index behavior (e.g. tooltips). By applying the following to direct children +of the body element here, we negate cascading side effects but allow momentum scrolling to be applied to the body */ + +.navbar, +.page-gutter, +.page-with-sidebar { + -webkit-overflow-scrolling: auto; +} diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index ec52f326eb9..1d8e64a0e4b 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -185,6 +185,10 @@ header.header-sidebar-pinned { @media (min-width: $screen-sm-min) { padding-right: $sidebar_collapsed_width; + + .merge-request-tabs-holder.affix { + right: $sidebar_collapsed_width; + } } .sidebar-collapsed-icon { @@ -207,6 +211,10 @@ header.header-sidebar-pinned { @media (min-width: $screen-md-min) { padding-right: $gutter_width; + + .merge-request-tabs-holder.affix { + right: $gutter_width; + } } &.with-overlay { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 7690d65de8e..eafe84570a8 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -16,6 +16,7 @@ $white-light: #fff; $white-normal: #ededed; $white-dark: #ececec; +$gray-lightest: #fdfdfd; $gray-light: #fafafa; $gray-lighter: #f9f9f9; $gray-normal: #f5f5f5; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 6e81c12aa55..d8fabbdcebe 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -1,4 +1,3 @@ -lex [v-cloak] { display: none; } @@ -132,7 +131,7 @@ lex } .board-blank-state { - height: 100%; + height: calc(100% - 49px); padding: $gl-padding; background-color: #fff; } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 2fbf0cf34bf..d6a55fbd464 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -195,7 +195,7 @@ .build-job { position: relative; - .fa { + .fa-arrow-right { position: absolute; left: 15px; top: 20px; @@ -205,14 +205,23 @@ &.active { font-weight: bold; - .fa { + .fa-arrow-right { display: block; } } + &.retried { + background-color: $gray-lightest; + } + &:hover { background-color: $row-hover; } + + .fa-refresh { + font-size: 13px; + margin-left: 3px; + } } } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index bdc82a8f0f5..fe6421f8b3f 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -52,7 +52,6 @@ background: #fff; color: #333; border-radius: 0 0 3px 3px; - -webkit-overflow-scrolling: auto; .unfold { cursor: pointer; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 820cc0fc991..12ee0a5dc3d 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -38,6 +38,14 @@ color: $gl-dark-link-color; } + .stop-env-link { + color: $table-text-gray; + + .stop-env-icon { + font-size: 14px; + } + } + .deployment { .build-column { diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 9bac6d46355..397f89f501a 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -66,7 +66,21 @@ text-overflow: ellipsis; vertical-align: middle; max-width: 100%; - } + } + } + + .label-type { + display: block; + margin-bottom: 10px; + margin-left: 50px; + + @media (min-width: $screen-sm-min) { + display: inline-block; + width: 100px; + margin-left: 10px; + margin-bottom: 0; + vertical-align: middle; + } } .label-description { @@ -209,6 +223,13 @@ } .label-subscribe-button { + .label-subscribe-button-icon { + &[disabled] { + opacity: 0.5; + pointer-events: none; + } + } + .label-subscribe-button-loading { display: none; } diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 4c0c0839fe2..9496234c773 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -53,6 +53,7 @@ margin: 0 0 10px; } + .login-footer { margin-top: 10px; @@ -124,19 +125,25 @@ display: -webkit-flex; display: flex; box-shadow: 0 0 0 1px $border-color; - border-top-right-radius: 2px; - border-top-left-radius: 2px; + border-top-right-radius: $border-radius-default; + border-top-left-radius: $border-radius-default; li { flex: 1; text-align: center; + &:first-of-type { + border-top-left-radius: $border-radius-default; + } + &:last-of-type { border-left: 1px solid $border-color; + border-top-right-radius: $border-radius-default; } &:not(.active) { background-color: $gray-light; + border-left: 1px solid $border-color; } a { @@ -164,6 +171,31 @@ } } + // Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long). + // These styles prevent this from breaking the layout, and only applied when providers are configured. + + .new-session-tabs.custom-provider-tabs { + flex-wrap: wrap; + + li { + min-width: 85px; + flex-basis: auto; + + // This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen. + // We are making somewhat of an assumption about the configuration here: that users do not have more than + // 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any + // of them. If either condition is false, this will work as expected. If both are true, there may be a missing border + // above one of the bottom row elements. If you know a better way, please implement it! + &:nth-child(n+5) { + border-top: 1px solid $border-color; + } + } + + a { + font-size: 16px; + } + } + .form-control { &:active, &:focus { @@ -197,6 +229,7 @@ .login-page { .col-sm-5.pull-right { float: none !important; + margin-bottom: 45px; } } } @@ -238,6 +271,27 @@ } .navless-container { - padding: 65px; // height of footer + bottom padding of email confirmation link + padding: 65px 15px; // height of footer + bottom padding of email confirmation link + + @media (max-width: $screen-xs-max) { + padding: 0 15px 65px; + } } } + +// For sign in pane only, to improve tab order, the following removes the submit button from +// normal document flow and pins it to the bottom of the form. For context, see !6867 & !6928 + +.login-box { + .new_user { + position: relative; + padding-bottom: 35px; + } + + .move-submit-down { + position: absolute; + width: 100%; + bottom: 0; + } +} + diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 101472278e2..70afa568554 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -183,6 +183,15 @@ .ci-coverage { float: right; } + + .stop-env-container { + color: $gl-text-color; + float: right; + + a { + color: $gl-text-color; + } + } } .mr_source_commit, @@ -429,11 +438,18 @@ } } -.merge-request-tabs { +.merge-request-tabs-holder { background-color: #fff; &.affix { top: 100px; + left: 0; z-index: 9; + transition: right .15s; + } + + &:not(.affix) .container-fluid { + padding-left: 0; + padding-right: 0; } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 7b71876b822..5b8dc7f8c40 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -23,10 +23,17 @@ .table.ci-table { min-width: 1200px; - .branch-commit { - width: 33%; + .pipeline-id { + color: $black; } + .branch-commit { + width: 30%; + + .branch-name { + max-width: 195px; + } + } } } @@ -73,6 +80,10 @@ border-top-width: 1px; } + .build.retried { + background-color: $gray-lightest; + } + .commit-link { .ci-status { @@ -88,6 +99,15 @@ } } + .avatar { + margin-left: 0; + float: none; + } + + .api { + color: $code-color; + } + .branch-commit { .branch-name { @@ -109,12 +129,11 @@ .fa { font-size: 12px; - color: $table-text-gray; + color: $gl-text-color; } .commit-id { color: $gl-link-color; - margin-right: 8px; } .commit-title { @@ -125,10 +144,6 @@ text-overflow: ellipsis; } - .avatar { - margin-left: 0; - } - .label { margin-right: 4px; } @@ -144,17 +159,11 @@ .icon-container { display: inline-block; - text-align: right; - width: 15px; - - .fa { - position: relative; - right: 3px; - } + width: 10px; - svg { - position: relative; - right: 1px; + &.commit-icon { + width: 15px; + text-align: center; } } @@ -360,10 +369,6 @@ &:hover { background-color: $gray-lighter; - - .dropdown-menu-toggle { - background-color: transparent; - } } &.playable { @@ -393,6 +398,15 @@ } } + .tooltip { + white-space: nowrap; + + .tooltip-inner { + overflow: hidden; + text-overflow: ellipsis; + } + } + .ci-status-text { width: 135px; white-space: nowrap; @@ -410,6 +424,7 @@ } .dropdown-menu-toggle { + background-color: transparent; border: none; width: auto; padding: 0; @@ -634,6 +649,10 @@ &.pipelines { + .ci-table { + min-width: 900px; + } + .content-list.pipelines { overflow: auto; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index d30f02340b9..fe7cf3c87e3 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -13,9 +13,18 @@ .new_project, .edit-project { + fieldset { - &.features .control-label { - font-weight: normal; + + &.features { + + .label-light { + margin-bottom: 0; + } + + .help-block { + margin-top: 0; + } } .form-group { @@ -40,6 +49,7 @@ } .input-group > div { + &:last-child { padding-right: 0; } @@ -47,6 +57,7 @@ @media (max-width: $screen-xs-max) { .input-group > div { + margin-bottom: 14px; &:last-child { @@ -60,6 +71,7 @@ } .input-group-addon { + &.static-namespace { height: 35px; border-radius: 3px; @@ -761,62 +773,6 @@ pre.light-well { .dropdown-menu { width: 300px; } - - &.from .compare-dropdown-toggle { - width: 237px; - } - - &.to .compare-dropdown-toggle { - width: 254px; - } - - .dropdown-toggle-text { - display: block; - height: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: 100%; - } -} - -.compare-ellipsis { - display: inline; -} - -@media (max-width: $screen-xs-max) { - .compare-form-group { - .input-group { - width: 100%; - - & > .compare-dropdown-toggle { - width: 100%; - } - } - - .dropdown-menu { - width: 100%; - } - } - - .compare-switch-container { - text-align: center; - padding: 0 0 $gl-padding; - - .commits-compare-switch { - float: none; - } - } - - .compare-ellipsis { - display: block; - text-align: center; - padding: 0 0 $gl-padding; - } - - .commits-compare-btn { - width: 100%; - } } .clearable-input { @@ -855,3 +811,30 @@ pre.light-well { border-bottom-right-radius: 0; } } + +.project-home-empty { + border-top: 0; + + .container-fluid { + background: none; + } + + p { + margin-left: auto; + margin-right: auto; + max-width: 650px; + } +} + +.project-feature-nested { + @media (min-width: $screen-sm-min) { + padding-left: 45px; + } +} + +.project-repo-select { + &.disabled { + opacity: 0.5; + pointer-events: none; + } +} diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb index 7c37f3155da..37a1a23178e 100644 --- a/app/controllers/admin/services_controller.rb +++ b/app/controllers/admin/services_controller.rb @@ -26,14 +26,10 @@ class Admin::ServicesController < Admin::ApplicationController private def services_templates - templates = [] - - Service.available_services_names.each do |service_name| + Service.available_services_names.map do |service_name| service_template = service_name.concat("_service").camelize.constantize - templates << service_template.where(template: true).first_or_create + service_template.where(template: true).first_or_create end - - templates end def service diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index bb32bc502e6..be86fa106f8 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -2,6 +2,7 @@ module IssuableActions extend ActiveSupport::Concern included do + before_action :labels, only: [:show, :new, :edit] before_action :authorize_destroy_issuable!, only: :destroy before_action :authorize_admin_issuable!, only: :bulk_update end @@ -25,6 +26,10 @@ module IssuableActions private + def labels + @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute + end + def authorize_destroy_issuable! unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable) return access_denied! diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb index 2a88350a4ca..d5031da867a 100644 --- a/app/controllers/dashboard/labels_controller.rb +++ b/app/controllers/dashboard/labels_controller.rb @@ -1,9 +1,9 @@ class Dashboard::LabelsController < Dashboard::ApplicationController def index - labels = Label.where(project_id: projects).select(:id, :title, :color).uniq(:title) + labels = LabelsFinder.new(current_user).execute respond_to do |format| - format.json { render json: labels } + format.json { render json: labels.as_json(only: [:id, :title, :color]) } end end end diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb new file mode 100644 index 00000000000..29528b2cfaa --- /dev/null +++ b/app/controllers/groups/labels_controller.rb @@ -0,0 +1,92 @@ +class Groups::LabelsController < Groups::ApplicationController + before_action :label, only: [:edit, :update, :destroy] + before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy] + before_action :save_previous_label_path, only: [:edit] + + respond_to :html + + def index + respond_to do |format| + format.html do + @labels = @group.labels.page(params[:page]) + end + + format.json do + available_labels = LabelsFinder.new(current_user, group_id: @group.id).execute + render json: available_labels.as_json(only: [:id, :title, :color]) + end + end + end + + def new + @label = @group.labels.new + @previous_labels_path = previous_labels_path + end + + def create + @label = @group.labels.create(label_params) + + if @label.valid? + redirect_to group_labels_path(@group) + else + render :new + end + end + + def edit + @previous_labels_path = previous_labels_path + end + + def update + if @label.update_attributes(label_params) + redirect_back_or_group_labels_path + else + render :edit + end + end + + def destroy + @label.destroy + + respond_to do |format| + format.html do + redirect_to group_labels_path(@group), notice: 'Label was removed' + end + format.js + end + end + + protected + + def authorize_admin_labels! + return render_404 unless can?(current_user, :admin_label, @group) + end + + def authorize_read_labels! + return render_404 unless can?(current_user, :read_label, @group) + end + + def label + @label ||= @group.labels.find(params[:id]) + end + + def label_params + params.require(:label).permit(:title, :description, :color) + end + + def redirect_back_or_group_labels_path(options = {}) + redirect_to previous_labels_path, options + end + + def previous_labels_path + session.fetch(:previous_labels_path, fallback_path) + end + + def fallback_path + group_labels_path(@group) + end + + def save_previous_label_path + session[:previous_labels_path] = URI(request.referer || '').path + end +end diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index 71eb56aed0b..a2b01ff43dc 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -72,10 +72,10 @@ module Projects def serialize_as_json(resource) resource.as_json( + labels: true, only: [:iid, :title, :confidential], include: { - assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, - labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] } + assignee: { only: [:id, :name, :username], methods: [:avatar_url] } }) end end diff --git a/app/controllers/projects/boards/lists_controller.rb b/app/controllers/projects/boards/lists_controller.rb index 76ae41319c4..67e3c9add81 100644 --- a/app/controllers/projects/boards/lists_controller.rb +++ b/app/controllers/projects/boards/lists_controller.rb @@ -76,9 +76,8 @@ module Projects resource.as_json( only: [:id, :list_type, :position], methods: [:title], - include: { - label: { only: [:id, :title, :description, :color, :priority] } - }) + label: true + ) end end end diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 3b2e35a7a05..fbe391fc58c 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -47,7 +47,9 @@ class Projects::BuildsController < Projects::ApplicationController def trace respond_to do |format| format.json do - render json: @build.trace_with_state(params[:state].presence).merge!(id: @build.id, status: @build.status) + state = params[:state].presence + render json: @build.trace_with_state(state: state). + merge!(id: @build.id, status: @build.status) end end end diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index a52c614b259..c2e7bf1ffec 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -13,7 +13,7 @@ class Projects::CommitsController < Projects::ApplicationController @commits = if search.present? - @repository.find_commits_by_message(search, @ref, @path, @limit, @offset).compact + @repository.find_commits_by_message(search, @ref, @path, @limit, @offset) else @repository.commits(@ref, path: @path, limit: @limit, offset: @offset) end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 58678f96879..ea22b2dcc15 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -2,11 +2,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController layout 'project' before_action :authorize_read_environment! before_action :authorize_create_environment!, only: [:new, :create] - before_action :authorize_update_environment!, only: [:edit, :update, :destroy] - before_action :environment, only: [:show, :edit, :update, :destroy] + before_action :authorize_create_deployment!, only: [:stop] + before_action :authorize_update_environment!, only: [:edit, :update] + before_action :environment, only: [:show, :edit, :update, :stop] def index - @environments = project.environments + @scope = params[:scope] + @all_environments = project.environments + @environments = + if @scope == 'stopped' + @all_environments.stopped + else + @all_environments.available + end end def show @@ -38,14 +46,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController end end - def destroy - if @environment.destroy - flash[:notice] = 'Environment was successfully removed.' - else - flash[:alert] = 'Failed to remove environment.' - end + def stop + return render_404 unless @environment.stoppable? - redirect_to namespace_project_environments_path(project.namespace, project) + new_action = @environment.stop!(current_user) + redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action]) end private diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 96041b07647..cb649264146 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -26,7 +26,9 @@ class Projects::IssuesController < Projects::ApplicationController @issues = issues_collection @issues = @issues.page(params[:page]) - @labels = @project.labels.where(title: params[:label_name]) + if params[:label_name].present? + @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute + end respond_to do |format| format.html diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index a6626df4826..4f855134368 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -3,21 +3,22 @@ class Projects::LabelsController < Projects::ApplicationController before_action :module_enabled before_action :label, only: [:edit, :update, :destroy] + before_action :find_labels, only: [:index, :set_priorities, :remove_priority] before_action :authorize_read_label! - before_action :authorize_admin_labels!, only: [ - :new, :create, :edit, :update, :generate, :destroy, :remove_priority, :set_priorities - ] + before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, + :generate, :destroy, :remove_priority, + :set_priorities] respond_to :js, :html def index - @labels = @project.labels.unprioritized.page(params[:page]) - @prioritized_labels = @project.labels.prioritized + @prioritized_labels = @available_labels.prioritized(@project) + @labels = @available_labels.unprioritized(@project).page(params[:page]) respond_to do |format| format.html format.json do - render json: @project.labels + render json: @available_labels.as_json(only: [:id, :title, :color]) end end end @@ -36,7 +37,7 @@ class Projects::LabelsController < Projects::ApplicationController end else respond_to do |format| - format.html { render 'new' } + format.html { render :new } format.json { render json: { message: @label.errors.messages }, status: 400 } end end @@ -49,7 +50,7 @@ class Projects::LabelsController < Projects::ApplicationController if @label.update_attributes(label_params) redirect_to namespace_project_labels_path(@project.namespace, @project) else - render 'edit' + render :edit end end @@ -68,6 +69,7 @@ class Projects::LabelsController < Projects::ApplicationController def destroy @label.destroy + @labels = find_labels respond_to do |format| format.html do @@ -80,20 +82,24 @@ class Projects::LabelsController < Projects::ApplicationController def remove_priority respond_to do |format| - if label.update_attribute(:priority, nil) + label = @available_labels.find(params[:id]) + + if label.unprioritize!(project) format.json { render json: label } else - message = label.errors.full_messages.uniq.join('. ') - format.json { render json: { message: message }, status: :unprocessable_entity } + format.json { head :unprocessable_entity } end end end def set_priorities Label.transaction do - params[:label_ids].each_with_index do |label_id, index| - label = @project.labels.find_by_id(label_id) - label.update_attribute(:priority, index) if label + available_labels_ids = @available_labels.where(id: params[:label_ids]).pluck(:id) + label_ids = params[:label_ids].select { |id| available_labels_ids.include?(id.to_i) } + + label_ids.each_with_index do |label_id, index| + label = @available_labels.find(label_id) + label.prioritize!(project, index) end end @@ -119,6 +125,10 @@ class Projects::LabelsController < Projects::ApplicationController end alias_method :subscribable_resource, :label + def find_labels + @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute.includes(:priorities) + end + def authorize_admin_labels! return render_404 unless can?(current_user, :admin_label, @project) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index a39b47b6d95..2ee53f7ceda 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -40,7 +40,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.preload(:target_project) - @labels = @project.labels.where(title: params[:label_name]) + if params[:label_name].present? + labels_params = { project_id: @project.id, title: params[:label_name] } + @labels = LabelsFinder.new(current_user, labels_params).execute + end respond_to do |format| format.html @@ -395,7 +398,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController status ||= "preparing" else - ci_service = @merge_request.source_project.ci_service + ci_service = @merge_request.source_project.try(:ci_service) status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service if ci_service.respond_to?(:commit_coverage) @@ -422,10 +425,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController project = environment.project deployment = environment.first_deployment_for(@merge_request.diff_head_commit) + stop_url = + if environment.stoppable? && can?(current_user, :create_deployment, environment) + stop_namespace_project_environment_path(project.namespace, project, environment) + end + { id: environment.id, name: environment.name, url: namespace_project_environment_path(project.namespace, project, environment), + stop_url: stop_url, external_url: environment.external_url, external_url_formatted: environment.formatted_external_url, deployed_at: deployment.try(:created_at), @@ -483,13 +492,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController @noteable = @merge_request @commits_count = @merge_request.commits.count - @pipeline = @merge_request.pipeline - @statuses = @pipeline.statuses.relevant if @pipeline - if @merge_request.locked_long_ago? @merge_request.unlock_mr @merge_request.close end + + define_pipelines_vars end # Discussion tab data is rendered on html responses of actions @@ -517,7 +525,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_widget_vars @pipeline = @merge_request.pipeline - @pipelines = [@pipeline].compact end def define_commit_vars @@ -544,6 +551,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController ) end + def define_pipelines_vars + @pipelines = @merge_request.all_pipelines + + if @pipelines.present? + @pipeline = @pipelines.first + @statuses = @pipeline.statuses.relevant + end + end + def define_new_vars @noteable = @merge_request @@ -559,10 +575,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commit = @merge_request.diff_head_commit @base_commit = @merge_request.diff_base_commit - @pipeline = @merge_request.pipeline - @statuses = @pipeline.statuses.relevant if @pipeline @note_counts = Note.where(commit_id: @commits.map(&:id)). group(:commit_id).count + + @labels = LabelsFinder.new(current_user, project_id: @project.id).execute + + define_pipelines_vars end def invalid_mr diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 37a86ed0523..2a07d154853 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -32,21 +32,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController current_user: current_user ) - if params[:group_ids].present? - group_ids = params[:group_ids].split(',') - groups = Group.where(id: group_ids) - - groups.each do |group| - next unless can?(current_user, :read_group, group) - - project.project_group_links.create( - group: group, - group_access: params[:access_level], - expires_at: params[:expires_at] - ) - end - end - redirect_to namespace_project_project_members_path(@project.namespace, @project) end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 62916270172..76b730198d4 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,4 +1,5 @@ class ProjectsController < Projects::ApplicationController + include IssuableCollections include ExtractsPath before_action :authenticate_user!, except: [:show, :activity, :refs] @@ -103,16 +104,7 @@ class ProjectsController < Projects::ApplicationController respond_to do |format| format.html do @notification_setting = current_user.notification_settings_for(@project) if current_user - - if @project.repository_exists? - if @project.empty_repo? - render 'projects/empty' - else - render :show - end - else - render 'projects/no_repo' - end + render_landing_page end format.atom do @@ -285,6 +277,26 @@ class ProjectsController < Projects::ApplicationController private + # Render project landing depending of which features are available + # So if page is not availble in the list it renders the next page + # + # pages list order: repository readme, wiki home, issues list, customize workflow + def render_landing_page + if @project.feature_available?(:repository, current_user) + return render 'projects/no_repo' unless @project.repository_exists? + render 'projects/empty' if @project.empty_repo? + else + if @project.wiki_enabled? + @wiki_home = @project.wiki.find_page('home', params[:version_id]) + elsif @project.feature_available?(:issues, current_user) + @issues = issues_collection + @issues = @issues.page(params[:page]) + end + + render :show + end + end + def determine_layout if [:new, :create].include?(action_name.to_sym) 'application' @@ -308,7 +320,8 @@ class ProjectsController < Projects::ApplicationController project_feature_attributes: [ :issues_access_level, :builds_access_level, - :wiki_access_level, :merge_requests_access_level, :snippets_access_level + :wiki_access_level, :merge_requests_access_level, + :snippets_access_level, :repository_access_level ] } diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 9f170428100..e27986ef95b 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -124,15 +124,12 @@ class IssuableFinder def labels return @labels if defined?(@labels) - if labels? && !filter_by_no_label? - @labels = Label.where(title: label_names) - - if projects - @labels = @labels.where(project: projects) + @labels = + if labels? && !filter_by_no_label? + LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute + else + Label.none end - else - @labels = Label.none - end end def assignee? @@ -274,8 +271,10 @@ class IssuableFinder items = items.without_label else items = items.with_label(label_names, params[:sort]) + if projects - items = items.where(labels: { project_id: projects }) + label_ids = LabelsFinder.new(current_user, project_ids: projects).execute.select(:id) + items = items.where(labels: { id: label_ids }) end end end diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb new file mode 100644 index 00000000000..6ace14a4bb5 --- /dev/null +++ b/app/finders/labels_finder.rb @@ -0,0 +1,92 @@ +class LabelsFinder < UnionFinder + def initialize(current_user, params = {}) + @current_user = current_user + @params = params + end + + def execute(authorized_only: true) + @authorized_only = authorized_only + + items = find_union(label_ids, Label) + items = with_title(items) + sort(items) + end + + private + + attr_reader :current_user, :params, :authorized_only + + def label_ids + label_ids = [] + + if project + label_ids << project.group.labels if project.group.present? + label_ids << project.labels + else + label_ids << Label.where(group_id: projects.group_ids) + label_ids << Label.where(project_id: projects.select(:id)) + end + + label_ids + end + + def sort(items) + items.reorder(title: :asc) + end + + def with_title(items) + items = items.where(title: title) if title + items + end + + def group_id + params[:group_id].presence + end + + def project_id + params[:project_id].presence + end + + def projects_ids + params[:project_ids].presence + end + + def title + params[:title].presence || params[:name].presence + end + + def project + return @project if defined?(@project) + + if project_id + @project = find_project + else + @project = nil + end + + @project + end + + def find_project + if authorized_only + available_projects.find_by(id: project_id) + else + Project.find_by(id: project_id) + end + end + + def projects + return @projects if defined?(@projects) + + @projects = authorized_only ? available_projects : Project.all + @projects = @projects.in_namespace(group_id) if group_id + @projects = @projects.where(id: projects_ids) if projects_ids + @projects = @projects.reorder(nil) + + @projects + end + + def available_projects + @available_projects ||= ProjectsFinder.new.execute(current_user) + end +end diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb index aa134cea31c..167b09e678f 100644 --- a/app/helpers/award_emoji_helper.rb +++ b/app/helpers/award_emoji_helper.rb @@ -1,9 +1,12 @@ module AwardEmojiHelper def toggle_award_url(awardable) - if @project - url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) + return url_for([:toggle_award_emoji, awardable]) unless @project + + if awardable.is_a?(Note) + # We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (4.5x) + toggle_award_emoji_namespace_project_note_url(@project.namespace, @project, awardable.id) else - url_for([:toggle_award_emoji, awardable]) + url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) end end end diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb new file mode 100644 index 00000000000..f3aaff9140d --- /dev/null +++ b/app/helpers/builds_helper.rb @@ -0,0 +1,8 @@ +module BuildsHelper + def sidebar_build_class(build, current_build) + build_class = '' + build_class += ' active' if build == current_build + build_class += ' retried' if build.retried? + build_class + end +end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index bfedcb1c42b..f8ded05c31a 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -154,7 +154,7 @@ module EventsHelper end def event_commit_title(message) - escape_once(truncate(message.split("\n").first, length: 70)) + (message.split("\n").first || "").truncate(70) rescue "--broken encoding" end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 670a7ca36f4..bccf64d1aac 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -94,6 +94,22 @@ module GitlabRoutingHelper namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args) end + def pipeline_url(pipeline, *args) + namespace_project_pipeline_url(pipeline.project.namespace, pipeline.project, pipeline.id, *args) + end + + def pipeline_build_url(pipeline, build, *args) + namespace_project_build_url(pipeline.project.namespace, pipeline.project, build.id, *args) + end + + def commits_url(entity, *args) + namespace_project_commits_url(entity.project.namespace, entity.project, entity.ref, *args) + end + + def commit_url(entity, *args) + namespace_project_commit_url(entity.project.namespace, entity.project, entity.sha, *args) + end + def project_snippet_url(entity, *args) namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args) end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 692fadd505f..03b2db1bc91 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -124,6 +124,10 @@ module IssuablesHelper end end + def issuable_filters_present + params[:search] || params[:author_id] || params[:assignee_id] || params[:milestone_title] || params[:label_name] + end + def issuables_count_for_state(issuable_type, state) issuables_finder = public_send("#{issuable_type}_finder") issuables_finder.params[:state] = state diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index b9f3d6c75c2..221a84b042f 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -4,9 +4,8 @@ module LabelsHelper # Link to a Label # # label - Label object to link to - # project - Project object which will be used as the context for the label's - # link. If omitted, defaults to `@project`, or the label's own - # project. + # subject - Project/Group object which will be used as the context for the + # label's link. If omitted, defaults to the label's own group/project. # type - The type of item the link will point to (:issue or # :merge_request). If omitted, defaults to :issue. # block - An optional block that will be passed to `link_to`, forming the @@ -15,15 +14,14 @@ module LabelsHelper # # Examples: # - # # Allow the generated link to use the label's own project + # # Allow the generated link to use the label's own subject # link_to_label(label) # - # # Force the generated link to use @project - # @project = Project.first - # link_to_label(label) + # # Force the generated link to use a provided group + # link_to_label(label, subject: Group.last) # # # Force the generated link to use a provided project - # link_to_label(label, project: Project.last) + # link_to_label(label, subject: Project.last) # # # Force the generated link to point to merge requests instead of issues # link_to_label(label, type: :merge_request) @@ -32,9 +30,8 @@ module LabelsHelper # link_to_label(label) { "My Custom Label Text" } # # Returns a String - def link_to_label(label, project: nil, type: :issue, tooltip: true, css_class: nil, &block) - project ||= @project || label.project - link = label_filter_path(project, label, type: type) + def link_to_label(label, subject: nil, type: :issue, tooltip: true, css_class: nil, &block) + link = label_filter_path(subject || label.subject, label, type: type) if block_given? link_to link, class: css_class, &block @@ -43,15 +40,40 @@ module LabelsHelper end end - def label_filter_path(project, label, type: issue) - send("namespace_project_#{type.to_s.pluralize}_path", - project.namespace, - project, - label_name: [label.name]) + def label_filter_path(subject, label, type: :issue) + case subject + when Group + send("#{type.to_s.pluralize}_group_path", + subject, + label_name: [label.name]) + when Project + send("namespace_project_#{type.to_s.pluralize}_path", + subject.namespace, + subject, + label_name: [label.name]) + end + end + + def edit_label_path(label) + case label + when GroupLabel then edit_group_label_path(label.group, label) + when ProjectLabel then edit_namespace_project_label_path(label.project.namespace, label.project, label) + end + end + + def destroy_label_path(label) + case label + when GroupLabel then group_label_path(label.group, label) + when ProjectLabel then namespace_project_label_path(label.project.namespace, label.project, label) + end end - def project_label_names - @project.labels.pluck(:title) + def toggle_subscription_data(label) + return unless label.is_a?(ProjectLabel) + + { + url: toggle_subscription_namespace_project_label_path(label.project.namespace, label.project, label) + } end def render_colored_label(label, label_suffix = '', tooltip: true) @@ -68,8 +90,8 @@ module LabelsHelper span.html_safe end - def render_colored_cross_project_label(label, tooltip: true) - label_suffix = label.project.name_with_namespace + def render_colored_cross_project_label(label, source_project = nil, tooltip: true) + label_suffix = source_project ? source_project.name_with_namespace : label.project.name_with_namespace label_suffix = " <i>in #{escape_once(label_suffix)}</i>" render_colored_label(label, label_suffix, tooltip: tooltip) end @@ -115,7 +137,10 @@ module LabelsHelper end def labels_filter_path + return group_labels_path(@group, :json) if @group + project = @target_project || @project + if project namespace_project_labels_path(project.namespace, project, :json) else @@ -124,11 +149,24 @@ module LabelsHelper end def label_subscription_status(label) - label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed' + case label + when GroupLabel then 'Subscribing to group labels is currently not supported.' + when ProjectLabel then label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed' + end end def label_subscription_toggle_button_text(label) - label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe' + case label + when GroupLabel then 'Subscribing to group labels is currently not supported.' + when ProjectLabel then label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe' + end + end + + def label_deletion_confirm_text(label) + case label + when GroupLabel then 'Remove this label? This will affect all projects within the group. Are you sure?' + when ProjectLabel then 'Remove this label? Are you sure?' + end end # Required for Banzai::Filter::LabelReferenceFilter diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 249cb44e9d5..a6659ea2fd1 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -86,11 +86,15 @@ module MergeRequestsHelper end def source_branch_with_namespace(merge_request) - branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch)) + namespace = merge_request.source_project_namespace + branch = merge_request.source_branch + + if merge_request.source_branch_exists? + namespace = link_to(namespace, project_path(merge_request.source_project)) + branch = link_to(branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch)) + end if merge_request.for_fork? - namespace = link_to(merge_request.source_project_namespace, - project_path(merge_request.source_project)) namespace + ":" + branch else branch diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index c3832cf5d65..a46f2c6e17d 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -50,6 +50,20 @@ module PreferencesHelper end def default_project_view - current_user ? current_user.project_view : 'readme' + return 'readme' unless current_user + + user_view = current_user.project_view + + if @project.feature_available?(:repository, current_user) + user_view + elsif user_view == "activity" + "activity" + elsif @project.wiki_enabled? + "wiki" + elsif @project.feature_available?(:issues, current_user) + "projects/issues/issues" + else + "customize_workflow" + end end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index e667c9e4e2e..d26b4018be6 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -134,16 +134,35 @@ module ProjectsHelper options = project_feature_options if @project.private? + level = @project.project_feature.send(field) options.delete('Everyone with access') - highest_available_option = options.values.max if @project.project_feature.send(field) == ProjectFeature::ENABLED + highest_available_option = options.values.max if level == ProjectFeature::ENABLED end options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field)) - content_tag(:select, options, name: "project[project_feature_attributes][#{field}]", id: "project_project_feature_attributes_#{field}", class: "pull-right form-control", data: { field: field }).html_safe + + content_tag( + :select, + options, + name: "project[project_feature_attributes][#{field}]", + id: "project_project_feature_attributes_#{field}", + class: "pull-right form-control #{repo_children_classes(field)}", + data: { field: field } + ).html_safe end private + def repo_children_classes(field) + needs_repo_check = [:merge_requests_access_level, :builds_access_level] + return unless needs_repo_check.include?(field) + + classes = "project-repo-select js-repo-select" + classes << " disabled" unless @project.feature_available?(:repository, current_user) + + classes + end + def get_project_nav_tabs(project, current_user) nav_tabs = [:home] @@ -155,12 +174,8 @@ module ProjectsHelper nav_tabs << :merge_requests end - if can?(current_user, :read_pipeline, project) - nav_tabs << :pipelines - end - if can?(current_user, :read_build, project) - nav_tabs << :builds + nav_tabs << :pipelines end if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project) @@ -435,4 +450,8 @@ module ProjectsHelper 'Everyone with access' => ProjectFeature::ENABLED } end + + def project_child_container_class(view_path) + view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}" + end end diff --git a/app/mailers/.gitkeep b/app/mailers/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 --- a/app/mailers/.gitkeep +++ /dev/null diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb new file mode 100644 index 00000000000..601c8b5cd62 --- /dev/null +++ b/app/mailers/emails/pipelines.rb @@ -0,0 +1,43 @@ +module Emails + module Pipelines + def pipeline_success_email(pipeline, to) + pipeline_mail(pipeline, to, 'succeeded') + end + + def pipeline_failed_email(pipeline, to) + pipeline_mail(pipeline, to, 'failed') + end + + private + + def pipeline_mail(pipeline, to, status) + @project = pipeline.project + @pipeline = pipeline + @merge_request = pipeline.merge_requests.first + add_headers + + mail(to: to, subject: pipeline_subject(status), skip_premailer: true) do |format| + format.html { render layout: false } + format.text + end + end + + def add_headers + add_project_headers + add_pipeline_headers + end + + def add_pipeline_headers + headers['X-GitLab-Pipeline-Id'] = @pipeline.id + headers['X-GitLab-Pipeline-Ref'] = @pipeline.ref + headers['X-GitLab-Pipeline-Status'] = @pipeline.status + end + + def pipeline_subject(status) + commit = @pipeline.short_sha + commit << " in #{@merge_request.to_reference}" if @merge_request + + subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.ref}", commit) + end + end +end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 2444702104e..eca6ec29767 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -7,6 +7,7 @@ class Notify < BaseMailer include Emails::Projects include Emails::Profile include Emails::Builds + include Emails::Pipelines include Emails::Members add_template_helper MergeRequestsHelper diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 87475119b23..a6b606d13de 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -133,13 +133,17 @@ module Ci latest_builds.where('stage_idx < ?', stage_idx) end - def trace_html - trace_with_state[:html] || '' + def trace_html(**args) + trace_with_state(**args)[:html] || '' end - def trace_with_state(state = nil) - trace_with_state = Ci::Ansi2html::convert(trace, state) if trace.present? - trace_with_state || {} + def trace_with_state(state: nil, last_lines: nil) + trace_ansi = trace(last_lines: last_lines) + if trace_ansi.present? + Ci::Ansi2html.convert(trace_ansi, state) + else + {} + end end def timeout @@ -222,9 +226,10 @@ module Ci raw_trace.present? end - def raw_trace + def raw_trace(last_lines: nil) if File.exist?(trace_file_path) - File.read(trace_file_path) + Gitlab::Ci::TraceReader.new(trace_file_path). + read(last_lines: last_lines) else # backward compatibility read_attribute :trace @@ -239,8 +244,8 @@ module Ci project.ci_id && File.exist?(old_path_to_trace) end - def trace - hide_secrets(raw_trace) + def trace(last_lines: nil) + hide_secrets(raw_trace(last_lines: last_lines)) end def trace_length diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index c7b9d6cc223..d5c1e03b461 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -19,7 +19,7 @@ module Ci validates_presence_of :status, unless: :importing? validate :valid_commit_sha, unless: :importing? - after_save :keep_around_commits, unless: :importing? + after_create :keep_around_commits, unless: :importing? delegate :stages, to: :statuses @@ -59,9 +59,6 @@ module Ci before_transition any => [:success, :failed, :canceled] do |pipeline| pipeline.finished_at = Time.now - end - - before_transition do |pipeline| pipeline.update_duration end @@ -154,7 +151,7 @@ module Ci def retryable? builds.latest.any? do |build| - build.failed? && build.retryable? + (build.failed? || build.canceled?) && build.retryable? end end diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb index be93435453b..b66ba08dc59 100644 --- a/app/models/concerns/expirable.rb +++ b/app/models/concerns/expirable.rb @@ -5,11 +5,15 @@ module Expirable scope :expired, -> { where('expires_at <= ?', Time.current) } end + def expired? + expires? && expires_at <= Time.current + end + def expires? expires_at.present? end def expires_soon? - expires_at < 7.days.from_now + expires? && expires_at < 7.days.from_now end end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 9f64f76721d..ef3e73a4072 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -5,6 +5,7 @@ module HasStatus STARTED_STATUSES = %w[running success failed skipped] ACTIVE_STATUSES = %w[pending running] COMPLETED_STATUSES = %w[success failed canceled] + ORDERED_STATUSES = %w[failed pending running canceled success skipped] class_methods do def status_sql diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index c4b42ad82c7..17c3b526c97 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -145,8 +145,14 @@ module Issuable end def order_labels_priority(excluded_labels: []) - condition_field = "#{table_name}.id" - highest_priority = highest_label_priority(name, condition_field, excluded_labels: excluded_labels).to_sql + params = { + target_type: name, + target_column: "#{table_name}.id", + project_column: "#{table_name}.#{project_foreign_key}", + excluded_labels: excluded_labels + } + + highest_priority = highest_label_priority(params).to_sql select("#{table_name}.*, (#{highest_priority}) AS highest_priority"). group(arel_table[:id]). @@ -230,18 +236,6 @@ module Issuable labels.order('title ASC').pluck(:title) end - def remove_labels - labels.delete_all - end - - def add_labels_by_names(label_names) - label_names.each do |label_name| - label = project.labels.create_with(color: Label::DEFAULT_COLOR). - find_or_create_by(title: label_name.strip) - self.labels << label - end - end - # Convert this Issuable class name to a format usable by Ability definitions # # Examples: diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 1ebecd86af9..12b23f00769 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -38,11 +38,13 @@ module Sortable private - def highest_label_priority(object_types, condition_field, excluded_labels: []) - query = Label.select(Label.arel_table[:priority].minimum). + def highest_label_priority(target_type:, target_column:, project_column:, excluded_labels: []) + query = Label.select(LabelPriority.arel_table[:priority].minimum). + left_join_priorities. joins(:label_links). - where(label_links: { target_type: object_types }). - where("label_links.target_id = #{condition_field}"). + where("label_priorities.project_id = #{project_column}"). + where(label_links: { target_type: target_type }). + where("label_links.target_id = #{target_column}"). reorder(nil) query.where.not(title: excluded_labels) if excluded_labels.present? diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 3d9902d496e..91d85c2279b 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base delegate :name, to: :environment, prefix: true - after_save :create_ref + after_create :create_ref def commit project.commit(sha) @@ -34,7 +34,7 @@ class Deployment < ActiveRecord::Base end def manual_actions - deployable.try(:other_actions) + @manual_actions ||= deployable.try(:other_actions) end def includes_commit?(commit) @@ -84,6 +84,17 @@ class Deployment < ActiveRecord::Base take end + def stop_action + return nil unless on_stop.present? + return nil unless manual_actions + + @stop_action ||= manual_actions.find_by(name: on_stop) + end + + def stoppable? + stop_action.present? + end + def formatted_deployment_time created_at.to_time.in_time_zone.to_s(:medium) end @@ -91,6 +102,6 @@ class Deployment < ActiveRecord::Base private def ref_path - File.join(environment.ref_path, 'deployments', id.to_s) + File.join(environment.ref_path, 'deployments', iid.to_s) end end diff --git a/app/models/email.rb b/app/models/email.rb index 32a412ab878..826d4f16edb 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -7,10 +7,8 @@ class Email < ActiveRecord::Base validates :email, presence: true, uniqueness: true, email: true validate :unique_email, if: ->(email) { email.email_changed? } - before_validation :cleanup_email - - def cleanup_email - self.email = self.email.downcase.strip + def email=(value) + write_attribute(:email, value.downcase.strip) end def unique_email diff --git a/app/models/environment.rb b/app/models/environment.rb index d970bc0a005..73f415c0ef0 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -19,6 +19,24 @@ class Environment < ActiveRecord::Base allow_nil: true, addressable_url: true + delegate :stop_action, to: :last_deployment, allow_nil: true + + scope :available, -> { with_state(:available) } + scope :stopped, -> { with_state(:stopped) } + + state_machine :state, initial: :available do + event :start do + transition stopped: :available + end + + event :stop do + transition available: :stopped + end + + state :available + state :stopped + end + def last_deployment deployments.last end @@ -53,8 +71,8 @@ class Environment < ActiveRecord::Base return nil unless ref - deployment_id = ref.split('/').last - deployments.find(deployment_id) + deployment_iid = ref.split('/').last + deployments.find_by(iid: deployment_iid) end def ref_path @@ -66,4 +84,14 @@ class Environment < ActiveRecord::Base external_url.gsub(/\A.*?:\/\//, '') end + + def stoppable? + available? && stop_action.present? + end + + def stop!(current_user) + return unless stoppable? + + stop_action.play(current_user) + end end diff --git a/app/models/event.rb b/app/models/event.rb index 0764cb8cabd..3993b35f96d 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -12,6 +12,7 @@ class Event < ActiveRecord::Base JOINED = 8 # User joined project LEFT = 9 # User left project DESTROYED = 10 + EXPIRED = 11 # User left project due to expiry RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour @@ -115,6 +116,10 @@ class Event < ActiveRecord::Base action == LEFT end + def expired? + action == EXPIRED + end + def destroyed? action == DESTROYED end @@ -124,7 +129,7 @@ class Event < ActiveRecord::Base end def membership_changed? - joined? || left? + joined? || left? || expired? end def created_project? @@ -184,6 +189,8 @@ class Event < ActiveRecord::Base 'joined' elsif left? 'left' + elsif expired? + 'removed due to membership expiration from' elsif destroyed? 'destroyed' elsif commented? diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index b7894c99846..fd9a8c1b8b7 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -29,11 +29,6 @@ class ExternalIssue @project end - # Pattern used to extract `JIRA-123` issue references from text - def self.reference_pattern - @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} - end - def to_reference(_from_project = nil) id end diff --git a/app/models/group.rb b/app/models/group.rb index a2f88cca828..6865e610718 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -19,6 +19,7 @@ class Group < Namespace has_many :project_group_links, dependent: :destroy has_many :shared_projects, through: :project_group_links, source: :project has_many :notification_settings, dependent: :destroy, as: :source + has_many :labels, class_name: 'GroupLabel' validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :visibility_level_allowed_by_projects @@ -67,7 +68,7 @@ class Group < Namespace end def web_url - Gitlab::Routing.url_helpers.group_url(self) + Gitlab::Routing.url_helpers.group_canonical_url(self) end def human_name diff --git a/app/models/group_label.rb b/app/models/group_label.rb new file mode 100644 index 00000000000..a698b532d19 --- /dev/null +++ b/app/models/group_label.rb @@ -0,0 +1,11 @@ +class GroupLabel < Label + belongs_to :group + + validates :group, presence: true + + alias_attribute :subject, :group + + def to_reference(source_project = nil, target_project = nil, format: :id) + super(source_project, target_project, format: format) + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index abd58e0454a..89158a50353 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -138,6 +138,10 @@ class Issue < ActiveRecord::Base reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE end + def self.project_foreign_key + 'project_id' + end + def self.sort(method, excluded_labels: []) case method.to_s when 'due_date_asc' then order_due_date_asc @@ -207,7 +211,13 @@ class Issue < ActiveRecord::Base note.all_references(current_user, extractor: ext) end - ext.merge_requests.select { |mr| mr.open? && mr.closes_issue?(self) } + merge_requests = ext.merge_requests.select(&:open?) + if merge_requests.any? + ids = MergeRequestsClosingIssues.where(merge_request_id: merge_requests.map(&:id), issue_id: id).pluck(:merge_request_id) + merge_requests.select { |mr| mr.id.in?(ids) } + else + [] + end end def moved? @@ -274,4 +284,16 @@ class Issue < ActiveRecord::Base def check_for_spam? project.public? end + + def as_json(options = {}) + super(options).tap do |json| + if options.has_key?(:labels) + json[:labels] = labels.as_json( + project: project, + only: [:id, :title, :description, :color], + methods: [:text_color] + ) + end + end + end end diff --git a/app/models/label.rb b/app/models/label.rb index e8e12e2904e..149fd98ecb3 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -15,34 +15,49 @@ class Label < ActiveRecord::Base default_value_for :color, DEFAULT_COLOR - belongs_to :project - has_many :lists, dependent: :destroy + has_many :priorities, class_name: 'LabelPriority' has_many :label_links, dependent: :destroy has_many :issues, through: :label_links, source: :target, source_type: 'Issue' has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest' validates :color, color: true, allow_blank: false - validates :project, presence: true, unless: Proc.new { |service| service.template? } # Don't allow ',' for label titles - validates :title, - presence: true, - format: { with: /\A[^,]+\z/ }, - uniqueness: { scope: :project_id } - - before_save :nullify_priority + validates :title, presence: true, format: { with: /\A[^,]+\z/ } + validates :title, uniqueness: { scope: [:group_id, :project_id] } default_scope { order(title: :asc) } - scope :templates, -> { where(template: true) } + scope :templates, -> { where(template: true) } + scope :with_title, ->(title) { where(title: title) } + + def self.prioritized(project) + joins(:priorities) + .where(label_priorities: { project_id: project }) + .reorder('label_priorities.priority ASC, labels.title ASC') + end + + def self.unprioritized(project) + labels = Label.arel_table + priorities = LabelPriority.arel_table + + label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin). + on(labels[:id].eq(priorities[:label_id]).and(priorities[:project_id].eq(project.id))). + join_sources - def self.prioritized - where.not(priority: nil).reorder(:priority, :title) + joins(label_priorities).where(priorities[:priority].eq(nil)) end - def self.unprioritized - where(priority: nil) + def self.left_join_priorities + labels = Label.arel_table + priorities = LabelPriority.arel_table + + label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin). + on(labels[:id].eq(priorities[:label_id])). + join_sources + + joins(label_priorities) end alias_attribute :name, :title @@ -77,6 +92,44 @@ class Label < ActiveRecord::Base nil end + def open_issues_count(user = nil, project = nil) + issues_count(user, project_id: project.try(:id) || project_id, state: 'opened') + end + + def closed_issues_count(user = nil, project = nil) + issues_count(user, project_id: project.try(:id) || project_id, state: 'closed') + end + + def open_merge_requests_count(user = nil, project = nil) + merge_requests_count(user, project_id: project.try(:id) || project_id, state: 'opened') + end + + def prioritize!(project, value) + label_priority = priorities.find_or_initialize_by(project_id: project.id) + label_priority.priority = value + label_priority.save! + end + + def unprioritize!(project) + priorities.where(project: project).delete_all + end + + def priority(project) + priorities.find_by(project: project).try(:priority) + end + + def template? + template + end + + def text_color + LabelsHelper.text_color_for_bg(self.color) + end + + def title=(value) + write_attribute(:title, sanitize_title(value)) if value.present? + end + ## # Returns the String necessary to reference this Label in Markdown # @@ -84,49 +137,47 @@ class Label < ActiveRecord::Base # # Examples: # - # Label.first.to_reference # => "~1" - # Label.first.to_reference(format: :name) # => "~\"bug\"" - # Label.first.to_reference(project) # => "gitlab-org/gitlab-ce~1" + # Label.first.to_reference # => "~1" + # Label.first.to_reference(format: :name) # => "~\"bug\"" + # Label.first.to_reference(project1, project2) # => "gitlab-org/gitlab-ce~1" # # Returns a String # - def to_reference(from_project = nil, format: :id) + def to_reference(source_project = nil, target_project = nil, format: :id) format_reference = label_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" - if cross_project_reference?(from_project) - project.to_reference + reference + if cross_project_reference?(source_project, target_project) + source_project.to_reference + reference else reference end end - def open_issues_count(user = nil) - issues.visible_to_user(user).opened.count - end - - def closed_issues_count(user = nil) - issues.visible_to_user(user).closed.count + def as_json(options = {}) + super(options).tap do |json| + json[:priority] = priority(options[:project]) if options.has_key?(:project) + end end - def open_merge_requests_count - merge_requests.opened.count - end + private - def template? - template + def cross_project_reference?(source_project, target_project) + source_project && target_project && source_project != target_project end - def text_color - LabelsHelper::text_color_for_bg(self.color) + def issues_count(user, params = {}) + IssuesFinder.new(user, params.reverse_merge(label_name: title, scope: 'all')) + .execute + .count end - def title=(value) - write_attribute(:title, sanitize_title(value)) if value.present? + def merge_requests_count(user, params = {}) + MergeRequestsFinder.new(user, params.reverse_merge(label_name: title, scope: 'all')) + .execute + .count end - private - def label_format_reference(format = :id) raise StandardError, 'Unknown format' unless [:id, :name].include?(format) @@ -137,10 +188,6 @@ class Label < ActiveRecord::Base end end - def nullify_priority - self.priority = nil if priority.blank? - end - def sanitize_title(value) CGI.unescapeHTML(Sanitize.clean(value.to_s)) end diff --git a/app/models/label_priority.rb b/app/models/label_priority.rb new file mode 100644 index 00000000000..5b85e0b6533 --- /dev/null +++ b/app/models/label_priority.rb @@ -0,0 +1,8 @@ +class LabelPriority < ActiveRecord::Base + belongs_to :project + belongs_to :label + + validates :project, :label, :priority, presence: true + validates :label_id, uniqueness: { scope: :project_id } + validates :priority, numericality: { only_integer: true, greater_than_or_equal_to: 0 } +end diff --git a/app/models/list.rb b/app/models/list.rb index eb87decdbc8..065d75bd1dc 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -26,6 +26,17 @@ class List < ActiveRecord::Base label? ? label.name : list_type.humanize end + def as_json(options = {}) + super(options).tap do |json| + if options.has_key?(:label) + json[:label] = label.as_json( + project: board.project, + only: [:id, :title, :description, :color] + ) + end + end + end + private def can_be_destroyed diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 125f26369d7..e4880973117 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -121,7 +121,11 @@ class ProjectMember < Member end def post_destroy_hook - event_service.leave_project(self.project, self.user) + if expired? + event_service.expired_leave_project(self.project, self.user) + else + event_service.leave_project(self.project, self.user) + end super end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 8c6905a442d..c476a3bb14e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -137,6 +137,10 @@ class MergeRequest < ActiveRecord::Base reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE end + def self.project_foreign_key + 'target_project_id' + end + # Returns all the merge requests from an ActiveRecord:Relation. # # This method uses a UNION as it usually operates on the result of @@ -322,21 +326,17 @@ class MergeRequest < ActiveRecord::Base def validate_fork return true unless target_project && source_project return true if target_project == source_project - return true unless forked_source_project_missing? + return true unless source_project_missing? errors.add :validate_fork, 'Source project is not a fork of the target project' end def closed_without_fork? - closed? && forked_source_project_missing? - end - - def closed_without_source_project? - closed? && !source_project + closed? && source_project_missing? end - def forked_source_project_missing? + def source_project_missing? return false unless for_fork? return true unless source_project @@ -344,9 +344,7 @@ class MergeRequest < ActiveRecord::Base end def reopenable? - return false if closed_without_fork? || closed_without_source_project? || merged? - - closed? + closed? && !source_project_missing? && source_branch_exists? end def ensure_merge_request_diff @@ -658,7 +656,7 @@ class MergeRequest < ActiveRecord::Base end def has_ci? - source_project.ci_service && commits.any? + source_project.try(:ci_service) && commits.any? end def branch_missing? @@ -690,12 +688,9 @@ class MergeRequest < ActiveRecord::Base @environments ||= begin - environments = source_project.environments_for( - source_branch, diff_head_commit) - environments += target_project.environments_for( - target_branch, diff_head_commit, with_tags: true) - - environments.uniq + envs = target_project.environments_for(target_branch, diff_head_commit, with_tags: true) + envs.concat(source_project.environments_for(source_branch, diff_head_commit)) if source_project + envs.uniq end end @@ -787,21 +782,21 @@ class MergeRequest < ActiveRecord::Base def all_pipelines return unless source_project - @all_pipelines ||= begin - sha = if persisted? - all_commits_sha - else - diff_head_sha - end - - source_project.pipelines.order(id: :desc). - where(sha: sha, ref: source_branch) - end + @all_pipelines ||= source_project.pipelines + .where(sha: all_commits_sha, ref: source_branch) + .order(id: :desc) end # Note that this could also return SHA from now dangling commits + # def all_commits_sha - merge_request_diffs.flat_map(&:commits_sha).uniq + if persisted? + merge_request_diffs.flat_map(&:commits_sha).uniq + elsif compare_commits + compare_commits.to_a.reverse.map(&:id) + else + [diff_head_sha] + end end def merge_commit diff --git a/app/models/project.rb b/app/models/project.rb index db3088677d8..af117f0acb0 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -32,8 +32,8 @@ class Project < ActiveRecord::Base default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } after_create :ensure_dir_exist + after_create :create_project_feature, unless: :project_feature after_save :ensure_dir_exist, if: :namespace_id_changed? - after_initialize :setup_project_feature # set last_activity_at to the same as created_at after_create :set_last_activity_at @@ -76,6 +76,7 @@ class Project < ActiveRecord::Base has_one :drone_ci_service, dependent: :destroy has_one :emails_on_push_service, dependent: :destroy has_one :builds_email_service, dependent: :destroy + has_one :pipelines_email_service, dependent: :destroy has_one :irker_service, dependent: :destroy has_one :pivotaltracker_service, dependent: :destroy has_one :hipchat_service, dependent: :destroy @@ -106,7 +107,7 @@ class Project < ActiveRecord::Base # Merge requests from source project should be kept when source project was removed has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest has_many :issues, dependent: :destroy - has_many :labels, dependent: :destroy + has_many :labels, dependent: :destroy, class_name: 'ProjectLabel' has_many :services, dependent: :destroy has_many :events, dependent: :destroy has_many :milestones, dependent: :destroy @@ -387,6 +388,10 @@ class Project < ActiveRecord::Base Project.count end end + + def group_ids + joins(:namespace).where(namespaces: { type: 'Group' }).pluck(:namespace_id) + end end def lfs_enabled? @@ -663,6 +668,10 @@ class Project < ActiveRecord::Base end end + def issue_reference_pattern + issues_tracker.reference_pattern + end + def default_issues_tracker? !external_issue_tracker end @@ -718,7 +727,7 @@ class Project < ActiveRecord::Base if template.nil? # If no template, we should create an instance. Ex `create_gitlab_ci_service` - self.send :"create_#{service_name}_service" + public_send("create_#{service_name}_service") else Service.create_from_template(self.id, template) end @@ -728,10 +737,8 @@ class Project < ActiveRecord::Base def create_labels Label.templates.each do |label| - label = label.dup - label.template = nil - label.project_id = self.id - label.save + params = label.attributes.except('id', 'template', 'created_at', 'updated_at') + Labels::FindOrCreateService.new(owner, self, params).execute end end @@ -1292,7 +1299,7 @@ class Project < ActiveRecord::Base environment_ids.where(ref: ref) end - environments.where(id: environment_ids).select do |environment| + environments.available.where(id: environment_ids).select do |environment| environment.includes_commit?(commit) end end @@ -1303,11 +1310,6 @@ class Project < ActiveRecord::Base "projects/#{id}/pushes_since_gc" end - # Prevents the creation of project_feature record for every project - def setup_project_feature - build_project_feature unless project_feature - end - def default_branch_protected? current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE @@ -1337,6 +1339,13 @@ class Project < ActiveRecord::Base shared_projects.any? end + # Similar to the normal callbacks that hook into the life cycle of an + # Active Record object, you can also define callbacks that get triggered + # when you add an object to an association collection. If any of these + # callbacks throw an exception, the object will not be added to the + # collection. Before you add a new board to the boards collection if you + # already have 1, 2, or n it will fail, but it if you have 0 that is lower + # than the number of permitted boards per project it won't fail. def validate_board_limit(board) raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS end diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 530f7d5a30e..b37ce1d3cf6 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -13,23 +13,26 @@ class ProjectFeature < ActiveRecord::Base # Enabled: enabled for everyone able to access the project # - # Permision levels + # Permission levels DISABLED = 0 PRIVATE = 10 ENABLED = 20 - FEATURES = %i(issues merge_requests wiki snippets builds) + FEATURES = %i(issues merge_requests wiki snippets builds repository) # Default scopes force us to unscope here since a service may need to check # permissions for a project in pending_delete # http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to belongs_to :project, -> { unscope(where: :pending_delete) } + validate :repository_children_level + default_value_for :builds_access_level, value: ENABLED, allows_nil: false default_value_for :issues_access_level, value: ENABLED, allows_nil: false default_value_for :merge_requests_access_level, value: ENABLED, allows_nil: false default_value_for :snippets_access_level, value: ENABLED, allows_nil: false default_value_for :wiki_access_level, value: ENABLED, allows_nil: false + default_value_for :repository_access_level, value: ENABLED, allows_nil: false def feature_available?(feature, user) raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature) @@ -57,6 +60,18 @@ class ProjectFeature < ActiveRecord::Base private + # Validates builds and merge requests access level + # which cannot be higher than repository access level + def repository_children_level + validator = lambda do |field| + level = public_send(field) || ProjectFeature::ENABLED + not_allowed = level > repository_access_level + self.errors.add(field, "cannot have higher visibility level than repository access level") if not_allowed + end + + %i(merge_requests_access_level builds_access_level).each(&validator) + end + def get_permission(user, level) case level when DISABLED diff --git a/app/models/project_label.rb b/app/models/project_label.rb new file mode 100644 index 00000000000..33c2b617715 --- /dev/null +++ b/app/models/project_label.rb @@ -0,0 +1,34 @@ +class ProjectLabel < Label + MAX_NUMBER_OF_PRIORITIES = 1 + + belongs_to :project + + validates :project, presence: true + + validate :permitted_numbers_of_priorities + validate :title_must_not_exist_at_group_level + + delegate :group, to: :project, allow_nil: true + + alias_attribute :subject, :project + + def to_reference(target_project = nil, format: :id) + super(project, target_project, format: format) + end + + private + + def title_must_not_exist_at_group_level + return unless group.present? && title_changed? + + if group.labels.with_title(self.title).exists? + errors.add(:title, :label_already_exists_at_group_level, group: group.name) + end + end + + def permitted_numbers_of_priorities + if priorities && priorities.size > MAX_NUMBER_OF_PRIORITIES + errors.add(:priorities, 'Number of permitted priorities exceeded') + end + end +end diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index fa66e5864b8..201b94b065b 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -43,7 +43,7 @@ class BuildsEmailService < Service end def can_test? - project.builds.count > 0 + project.builds.any? end def disabled_title diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index afebd3b6a12..660a8ae3421 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -1,5 +1,12 @@ class HipchatService < Service + include ActionView::Helpers::SanitizeHelper + MAX_COMMITS = 3 + HIPCHAT_ALLOWED_TAGS = %w[ + a b i strong em br img pre code + table th tr td caption colgroup col thead tbody tfoot + ul ol li dl dt dd + ] prop_accessor :token, :room, :server, :notify, :color, :api_version boolean_accessor :notify_only_broken_builds @@ -88,6 +95,10 @@ class HipchatService < Service end end + def render_line(text) + markdown(text.lines.first.chomp, pipeline: :single_line) if text + end + def create_push_message(push) ref_type = Gitlab::Git.tag_ref?(push[:ref]) ? 'tag' : 'branch' ref = Gitlab::Git.ref_name(push[:ref]) @@ -110,7 +121,7 @@ class HipchatService < Service message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)" push[:commits].take(MAX_COMMITS).each do |commit| - message << "<br /> - #{commit[:message].lines.first} (<a href=\"#{commit[:url]}\">#{commit[:id][0..5]}</a>)" + message << "<br /> - #{render_line(commit[:message])} (<a href=\"#{commit[:url]}\">#{commit[:id][0..5]}</a>)" end if push[:commits].count > MAX_COMMITS @@ -121,12 +132,22 @@ class HipchatService < Service message end - def format_body(body) - if body - body = body.truncate(200, separator: ' ', omission: '...') - end + def markdown(text, options = {}) + return "" unless text + + context = { + project: project, + pipeline: :email + } + + Banzai.render(text, context) - "<pre>#{body}</pre>" + context.merge!(options) + + html = Banzai.post_process(Banzai.render(text, context), context) + sanitized_html = sanitize(html, tags: HIPCHAT_ALLOWED_TAGS, attributes: %w[href title alt]) + + sanitized_html.truncate(200, separator: ' ', omission: '...') end def create_issue_message(data) @@ -134,7 +155,7 @@ class HipchatService < Service obj_attr = data[:object_attributes] obj_attr = HashWithIndifferentAccess.new(obj_attr) - title = obj_attr[:title] + title = render_line(obj_attr[:title]) state = obj_attr[:state] issue_iid = obj_attr[:iid] issue_url = obj_attr[:url] @@ -143,10 +164,7 @@ class HipchatService < Service issue_link = "<a href=\"#{issue_url}\">issue ##{issue_iid}</a>" message = "#{user_name} #{state} #{issue_link} in #{project_link}: <b>#{title}</b>" - if description - description = format_body(description) - message << description - end + message << "<pre>#{markdown(description)}</pre>" message end @@ -159,23 +177,20 @@ class HipchatService < Service merge_request_id = obj_attr[:iid] state = obj_attr[:state] description = obj_attr[:description] - title = obj_attr[:title] + title = render_line(obj_attr[:title]) merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}" merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>" message = "#{user_name} #{state} #{merge_request_link} in " \ "#{project_link}: <b>#{title}</b>" - if description - description = format_body(description) - message << description - end + message << "<pre>#{markdown(description)}</pre>" message end def format_title(title) - "<b>" + title.lines.first.chomp + "</b>" + "<b>#{render_line(title)}</b>" end def create_note_message(data) @@ -186,11 +201,13 @@ class HipchatService < Service note = obj_attr[:note] note_url = obj_attr[:url] noteable_type = obj_attr[:noteable_type] + commit_id = nil case noteable_type when "Commit" commit_attr = HashWithIndifferentAccess.new(data[:commit]) - subject_desc = commit_attr[:id] + commit_id = commit_attr[:id] + subject_desc = commit_id subject_desc = Commit.truncate_sha(subject_desc) subject_type = "commit" title = format_title(commit_attr[:message]) @@ -218,10 +235,7 @@ class HipchatService < Service message = "#{user_name} commented on #{subject_html} in #{project_link}: " message << title - if note - note = format_body(note) - message << note - end + message << "<pre>#{markdown(note, ref: commit_id)}</pre>" message end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index d1df6d0292f..b26ddd518d7 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -3,6 +3,12 @@ class IssueTrackerService < Service default_value_for :category, 'issue_tracker' + # Pattern used to extract links from comments + # Override this method on services that uses different patterns + def reference_pattern + @reference_pattern ||= %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)} + end + def default? default end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 97bcbacf2b9..f81b66fd219 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -13,6 +13,11 @@ class JiraService < IssueTrackerService before_update :reset_password + # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1 + def reference_pattern + @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} + end + def reset_password # don't reset the password if a new one is provided if api_url_changed? && !password_touched? diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb new file mode 100644 index 00000000000..ec3c1bc85ee --- /dev/null +++ b/app/models/project_services/pipelines_email_service.rb @@ -0,0 +1,96 @@ +class PipelinesEmailService < Service + prop_accessor :recipients + boolean_accessor :add_pusher + boolean_accessor :notify_only_broken_pipelines + validates :recipients, + presence: true, + if: ->(s) { s.activated? && !s.add_pusher? } + + def initialize_properties + self.properties ||= { notify_only_broken_pipelines: true } + end + + def title + 'Pipelines emails' + end + + def description + 'Email the pipelines status to a list of recipients.' + end + + def to_param + 'pipelines_email' + end + + def supported_events + %w[pipeline] + end + + def execute(data, force: false) + return unless supported_events.include?(data[:object_kind]) + return unless force || should_pipeline_be_notified?(data) + + all_recipients = retrieve_recipients(data) + + return unless all_recipients.any? + + pipeline = Ci::Pipeline.find(data[:object_attributes][:id]) + Ci::SendPipelineNotificationService.new(pipeline).execute(all_recipients) + end + + def can_test? + project.pipelines.any? + end + + def disabled_title + 'Please setup a pipeline on your repository.' + end + + def test_data(project, user) + data = Gitlab::DataBuilder::Pipeline.build(project.pipelines.last) + data[:user] = user.hook_attrs + data + end + + def fields + [ + { type: 'textarea', + name: 'recipients', + placeholder: 'Emails separated by comma' }, + { type: 'checkbox', + name: 'add_pusher', + label: 'Add pusher to recipients list' }, + { type: 'checkbox', + name: 'notify_only_broken_pipelines' }, + ] + end + + def test(data) + result = execute(data, force: true) + + { success: true, result: result } + rescue StandardError => error + { success: false, result: error } + end + + def should_pipeline_be_notified?(data) + case data[:object_attributes][:status] + when 'success' + !notify_only_broken_pipelines? + when 'failed' + true + else + false + end + end + + def retrieve_recipients(data) + all_recipients = recipients.to_s.split(',').reject(&:blank?) + + if add_pusher? && data[:user].try(:[], :email) + all_recipients << data[:user][:email] + end + + all_recipients + end +end diff --git a/app/models/repository.rb b/app/models/repository.rb index 72e473871fa..4ae9c20726f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -109,6 +109,10 @@ class Repository end def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0) + unless exists? && has_visible_content? && query.present? + return [] + end + ref ||= root_ref args = %W( @@ -117,9 +121,8 @@ class Repository ) args = args.concat(%W(-- #{path})) if path.present? - git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp) - commits = git_log_results.map { |c| commit(c) } - commits + git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines + git_log_results.map { |c| commit(c.chomp) }.compact end def find_branch(name, fresh_repo: true) @@ -416,6 +419,17 @@ class Repository @exists = nil end + # expire cache that doesn't depend on repository data (when expiring) + def expire_content_cache + expire_tags_cache + expire_tag_count_cache + expire_branches_cache + expire_branch_count_cache + expire_root_ref_cache + expire_emptiness_caches + expire_exists_cache + end + # Runs code after a repository has been created. def after_create expire_exists_cache @@ -431,14 +445,7 @@ class Repository expire_cache if exists? - # expire cache that don't depend on repository data (when expiring) - expire_tags_cache - expire_tag_count_cache - expire_branches_cache - expire_branch_count_cache - expire_root_ref_cache - expire_emptiness_caches - expire_exists_cache + expire_content_cache repository_event(:remove_repository) end @@ -470,14 +477,13 @@ class Repository end def before_import - expire_emptiness_caches - expire_exists_cache + expire_content_cache end # Runs code after a repository has been forked/imported. def after_import - expire_emptiness_caches - expire_exists_cache + expire_content_cache + build_cache end # Runs code after a new commit has been pushed. diff --git a/app/models/service.rb b/app/models/service.rb index 66c804f2b06..625fbc48302 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -196,12 +196,13 @@ class Service < ActiveRecord::Base end def self.available_services_names - %w( + %w[ asana assembla bamboo buildkite builds_email + pipelines_email bugzilla campfire custom_issue_tracker @@ -218,7 +219,7 @@ class Service < ActiveRecord::Base redmine slack teamcity - ) + ] end def self.create_from_template(project_id, template) diff --git a/app/models/todo.rb b/app/models/todo.rb index 6ae9956ade5..11c072dd000 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -52,7 +52,13 @@ class Todo < ActiveRecord::Base # Todos with highest priority first then oldest todos # Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue" def order_by_labels_priority - highest_priority = highest_label_priority(["Issue", "MergeRequest"], "todos.target_id").to_sql + params = { + target_type: ['Issue', 'MergeRequest'], + target_column: "todos.target_id", + project_column: "todos.project_id" + } + + highest_priority = highest_label_priority(params).to_sql select("#{table_name}.*, (#{highest_priority}) AS highest_priority"). order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')). diff --git a/app/policies/group_label_policy.rb b/app/policies/group_label_policy.rb new file mode 100644 index 00000000000..7b34aa182eb --- /dev/null +++ b/app/policies/group_label_policy.rb @@ -0,0 +1,5 @@ +class GroupLabelPolicy < BasePolicy + def rules + delegate! @subject.group + end +end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 97ff6233968..b65fb68cd88 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -19,6 +19,7 @@ class GroupPolicy < BasePolicy if master can! :create_projects can! :admin_milestones + can! :admin_label end # Only group owner and administrators can admin group diff --git a/app/policies/project_label_policy.rb b/app/policies/project_label_policy.rb new file mode 100644 index 00000000000..b12b4c5166b --- /dev/null +++ b/app/policies/project_label_policy.rb @@ -0,0 +1,5 @@ +class ProjectLabelPolicy < BasePolicy + def rules + delegate! @subject.project + end +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index be4721d7a51..fbb3d4507d6 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -162,11 +162,13 @@ class ProjectPolicy < BasePolicy end def disabled_features! + repository_enabled = project.feature_available?(:repository, user) + unless project.feature_available?(:issues, user) cannot!(*named_abilities(:issue)) end - unless project.feature_available?(:merge_requests, user) + unless project.feature_available?(:merge_requests, user) && repository_enabled cannot!(*named_abilities(:merge_request)) end @@ -183,13 +185,21 @@ class ProjectPolicy < BasePolicy cannot!(*named_abilities(:wiki)) end - unless project.feature_available?(:builds, user) + unless project.feature_available?(:builds, user) && repository_enabled cannot!(*named_abilities(:build)) cannot!(*named_abilities(:pipeline)) cannot!(*named_abilities(:environment)) cannot!(*named_abilities(:deployment)) end + unless repository_enabled + cannot! :push_code + cannot! :push_code_to_protected_branches + cannot! :download_code + cannot! :fork_project + cannot! :read_commit_status + end + unless project.container_registry_enabled cannot!(*named_abilities(:container_image)) end diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb index abc7aeece39..fe0d762ccd2 100644 --- a/app/services/boards/lists/create_service.rb +++ b/app/services/boards/lists/create_service.rb @@ -3,7 +3,7 @@ module Boards class CreateService < BaseService def execute(board) List.transaction do - label = project.labels.find(params[:label_id]) + label = available_labels.find(params[:label_id]) position = next_position(board) create_list(board, label, position) @@ -12,6 +12,10 @@ module Boards private + def available_labels + LabelsFinder.new(current_user, project_id: project.id).execute + end + def next_position(board) max_position = board.lists.movable.maximum(:position) max_position.nil? ? 0 : max_position.succ diff --git a/app/services/boards/lists/generate_service.rb b/app/services/boards/lists/generate_service.rb index d8048f1c67e..939f9bfd068 100644 --- a/app/services/boards/lists/generate_service.rb +++ b/app/services/boards/lists/generate_service.rb @@ -19,8 +19,7 @@ module Boards end def find_or_create_label(params) - project.labels.create_with(color: params[:color]) - .find_or_create_by(name: params[:name]) + ::Labels::FindOrCreateService.new(current_user, project, params).execute end def label_params diff --git a/app/services/ci/send_pipeline_notification_service.rb b/app/services/ci/send_pipeline_notification_service.rb new file mode 100644 index 00000000000..ceb182801f7 --- /dev/null +++ b/app/services/ci/send_pipeline_notification_service.rb @@ -0,0 +1,19 @@ +module Ci + class SendPipelineNotificationService + attr_reader :pipeline + + def initialize(new_pipeline) + @pipeline = new_pipeline + end + + def execute(recipients) + email_template = "pipeline_#{pipeline.status}_email" + + return unless Notify.respond_to?(email_template) + + recipients.each do |to| + Notify.public_send(email_template, pipeline, to).deliver_later + end + end + end +end diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb index ff9a8310a8c..8ae15ad32f4 100644 --- a/app/services/create_deployment_service.rb +++ b/app/services/create_deployment_service.rb @@ -6,7 +6,13 @@ class CreateDeploymentService < BaseService ActiveRecord::Base.transaction do @deployable = deployable - @environment = prepare_environment + + @environment = environment + @environment.external_url = expanded_url if expanded_url + @environment.fire_state_event(action) + + return unless @environment.save + return if @environment.stopped? deploy.tap do |deployment| deployment.update_merge_request_metrics! @@ -27,13 +33,12 @@ class CreateDeploymentService < BaseService tag: params[:tag], sha: params[:sha], user: current_user, - deployable: @deployable) + deployable: @deployable, + on_stop: options[:on_stop]) end - def prepare_environment - project.environments.find_or_create_by(name: expanded_name) do |environment| - environment.external_url = expanded_url - end + def environment + @environment ||= project.environments.find_or_create_by(name: expanded_name) end def expanded_name @@ -61,4 +66,8 @@ class CreateDeploymentService < BaseService def variables params[:variables] || [] end + + def action + options[:action] || 'start' + end end diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index 07fc77001a5..e24cc66e0fe 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -62,6 +62,10 @@ class EventCreateService create_event(project, current_user, Event::LEFT) end + def expired_leave_project(project, current_user) + create_event(project, current_user, Event::EXPIRED) + end + def create_project(project, current_user) create_event(project, current_user, Event::CREATED) end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 57d521f2fea..bb92cd80cc9 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -80,17 +80,18 @@ class IssuableBaseService < BaseService def filter_labels_in_param(key) return if params[key].to_a.empty? - params[key] = project.labels.where(id: params[key]).pluck(:id) + params[key] = available_labels.where(id: params[key]).pluck(:id) end def find_or_create_label_ids labels = params.delete(:labels) return unless labels - params[:label_ids] = labels.split(",").map do |label_name| - project.labels.create_with(color: Label::DEFAULT_COLOR) - .find_or_create_by(title: label_name.strip) - .id + 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 end @@ -111,6 +112,10 @@ class IssuableBaseService < BaseService new_label_ids end + def available_labels + LabelsFinder.new(current_user, project_id: @project.id).execute + end + def merge_slash_commands_into_params!(issuable) description, command_params = SlashCommands::InterpretService.new(project, current_user). diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index ab667456db7..a2a5f57d069 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -52,8 +52,12 @@ module Issues end def cloneable_label_ids - @new_project.labels - .where(title: @old_issue.labels.pluck(:title)).pluck(:id) + params = { + project_id: @new_project.id, + title: @old_issue.labels.pluck(:title) + } + + LabelsFinder.new(current_user, params).execute.pluck(:id) end def cloneable_milestone_id diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb new file mode 100644 index 00000000000..74291312c4e --- /dev/null +++ b/app/services/labels/find_or_create_service.rb @@ -0,0 +1,33 @@ +module Labels + class FindOrCreateService + def initialize(current_user, project, params = {}) + @current_user = current_user + @group = project.group + @project = project + @params = params.dup + end + + def execute + find_or_create_label + end + + private + + attr_reader :current_user, :group, :project, :params + + def available_labels + @available_labels ||= LabelsFinder.new(current_user, project_id: project.id).execute + end + + def find_or_create_label + new_label = available_labels.find_by(title: title) + new_label ||= project.labels.create(params) + + new_label + end + + def title + params[:title] || params[:name] + end + end +end diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb new file mode 100644 index 00000000000..514679ed29d --- /dev/null +++ b/app/services/labels/transfer_service.rb @@ -0,0 +1,78 @@ +# Labels::TransferService class +# +# User for recreate the missing group labels at project level +# +module Labels + class TransferService + def initialize(current_user, old_group, project) + @current_user = current_user + @old_group = old_group + @project = project + end + + def execute + return unless old_group.present? + + Label.transaction do + labels_to_transfer.find_each do |label| + new_label_id = find_or_create_label!(label) + + next if new_label_id == label.id + + update_label_links(group_labels_applied_to_issues, old_label_id: label.id, new_label_id: new_label_id) + update_label_links(group_labels_applied_to_merge_requests, old_label_id: label.id, new_label_id: new_label_id) + update_label_priorities(old_label_id: label.id, new_label_id: new_label_id) + end + end + end + + private + + attr_reader :current_user, :old_group, :project + + def labels_to_transfer + label_ids = [] + label_ids << group_labels_applied_to_issues.select(:id) + label_ids << group_labels_applied_to_merge_requests.select(:id) + + union = Gitlab::SQL::Union.new(label_ids) + + Label.where("labels.id IN (#{union.to_sql})").reorder(nil).uniq + end + + def group_labels_applied_to_issues + Label.joins(:issues). + where( + issues: { project_id: project.id }, + labels: { type: 'GroupLabel', group_id: old_group.id } + ) + end + + def group_labels_applied_to_merge_requests + Label.joins(:merge_requests). + where( + merge_requests: { target_project_id: project.id }, + labels: { type: 'GroupLabel', group_id: old_group.id } + ) + end + + def find_or_create_label!(label) + params = label.attributes.slice('title', 'description', 'color') + new_label = FindOrCreateService.new(current_user, project, params).execute + + new_label.id + end + + def update_label_links(labels, old_label_id:, new_label_id:) + LabelLink.joins(:label). + merge(labels). + where(label_id: old_label_id). + update_all(label_id: new_label_id) + end + + def update_label_priorities(old_label_id:, new_label_id:) + LabelPriority.where(project_id: project.id, label_id: old_label_id). + update_all(label_id: new_label_id) + end + end +end diff --git a/app/services/merge_requests/assign_issues_service.rb b/app/services/merge_requests/assign_issues_service.rb index f636e5fec4f..066efa1acc3 100644 --- a/app/services/merge_requests/assign_issues_service.rb +++ b/app/services/merge_requests/assign_issues_service.rb @@ -4,7 +4,7 @@ module MergeRequests @assignable_issues ||= begin if current_user == merge_request.author closes_issues.select do |issue| - !issue.assignee_id? && can?(current_user, :admin_issue, issue) + !issue.is_a?(ExternalIssue) && !issue.assignee_id? && can?(current_user, :admin_issue, issue) end else [] diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index b037780c431..ab9056a3250 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -11,14 +11,14 @@ module MergeRequests def execute(merge_request) @merge_request = merge_request - return error('Merge request is not mergeable') unless @merge_request.mergeable? + return log_merge_error('Merge request is not mergeable', true) unless @merge_request.mergeable? merge_request.in_locked_state do if commit after_merge success else - error('Can not merge changes') + log_merge_error('Can not merge changes', true) end end end @@ -46,8 +46,8 @@ module MergeRequests merge_request.update(merge_error: e.message) false rescue StandardError => e - merge_request.update(merge_error: "Something went wrong during merge") - Rails.logger.error(e.message) + merge_request.update(merge_error: "Something went wrong during merge: #{e.message}") + log_merge_error(e.message) false ensure merge_request.update(in_progress_merge_commit_sha: nil) @@ -65,5 +65,17 @@ module MergeRequests def branch_deletion_user @merge_request.force_remove_source_branch? ? @merge_request.author : current_user end + + def log_merge_error(message, http_error = false) + Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{message}") + + error(message) if http_error + end + + def merge_request_info + project = merge_request.project + + "#{project.to_reference}#{merge_request.to_reference}" + end end end diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index f578f8dbea2..015f2828921 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -13,7 +13,7 @@ module Projects end def labels - @project.labels.select([:title, :color]) + LabelsFinder.new(current_user, project_id: project.id).execute.select([:title, :color]) end def commands(noteable, type) diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index bc7f8bf433b..28470f59807 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -28,6 +28,7 @@ module Projects Project.transaction do old_path = project.path_with_namespace old_namespace = project.namespace + old_group = project.group new_path = File.join(new_namespace.try(:path) || '', project.path) if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present? @@ -57,6 +58,9 @@ module Projects # Move wiki repo also if present gitlab_shell.mv_repository(project.repository_storage_path, "#{old_path}.wiki", "#{new_path}.wiki") + # Move missing group labels to project + Labels::TransferService.new(current_user, old_group, project).execute + # clear project cached events project.reset_events_cache diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index e4ae3dec8aa..5a81194a5f4 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -116,8 +116,10 @@ module SlashCommands desc 'Add label(s)' params '~label1 ~"label 2"' condition do + available_labels = LabelsFinder.new(current_user, project_id: project.id).execute + current_user.can?(:"admin_#{issuable.to_ability_name}", project) && - project.labels.any? + available_labels.any? end command :label do |labels_param| label_ids = find_label_ids(labels_param) @@ -248,7 +250,7 @@ module SlashCommands def find_label_ids(labels_param) label_ids_by_reference = extract_references(labels_param, :label).map(&:id) - labels_ids_by_name = @project.labels.where(name: labels_param.split).select(:id) + labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id) label_ids_by_reference | labels_ids_by_name end diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index b760b42fde0..37bb6a3b0e0 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -75,4 +75,4 @@ - @runners.each do |runner| = render "admin/runners/runner", runner: runner - = paginate @runners + = paginate @runners, theme: "gitlab" diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 10fea1996aa..73038164056 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -67,7 +67,7 @@ = form_for [:admin, project.namespace.becomes(Namespace), project, project.runner_projects.new] do |f| = f.hidden_field :runner_id, value: @runner.id = f.submit 'Enable', class: 'btn btn-xs' - = paginate @projects + = paginate @projects, theme: "gitlab" .col-md-6 %h4 Recent builds served by this Runner diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index a96b579c593..525e7d99d71 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -5,6 +5,8 @@ %div.form-group = f.label :password = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required." + %div.submit-container.move-submit-down + = f.submit "Sign in", class: "btn btn-save" - if devise_mapping.rememberable? .remember-me.checkbox %label{for: "user_remember_me"} @@ -12,5 +14,3 @@ %span Remember me .pull-right = link_to "Forgot your password?", new_password_path(resource_name) - %div.submit-container - = f.submit "Sign in", class: "btn btn-save" diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml index e82a08cdb0c..1d381ad7893 100644 --- a/app/views/devise/sessions/_new_crowd.html.haml +++ b/app/views/devise/sessions/_new_crowd.html.haml @@ -1,6 +1,6 @@ = form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user', class: 'show-gl-field-errors') do .form-group - = label_tag 'Username or email', for: :username + = label_tag :username, 'Username or email' = text_field_tag :username, nil, {class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true } .form-group = label_tag :password @@ -10,4 +10,4 @@ %label{for: "remember_me"} = check_box_tag :remember_me, '1', false, id: 'remember_me' %span Remember me - = button_tag "Sign in", class: "btn-save btn" + = submit_tag "Sign in", class: "btn-save btn" diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index b26efbb4535..c18bc2ac413 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -1,6 +1,6 @@ = form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "show-gl-field-errors") do .form-group - = label_tag "#{server['label']} Login", for: :username + = label_tag :username, "#{server['label']} Username" = text_field_tag :username, nil, {class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true } .form-group = label_tag :password @@ -10,4 +10,4 @@ %label{for: "remember_me"} = check_box_tag :remember_me, '1', false, id: 'remember_me' %span Remember me - = button_tag "Sign in", class: "btn-save btn" + = submit_tag "Sign in", class: "btn-save btn" diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index 0e865b807c1..fd77cdbee2e 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -10,7 +10,7 @@ = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user show-gl-field-errors' }) do |f| - resource_params = params[resource_name].presence || params = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) - .form-group + %div = f.label 'Two-Factor Authentication code', name: :otp_attempt = f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.' %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml index 810dd5ab687..86edaf14e43 100644 --- a/app/views/devise/shared/_signin_box.html.haml +++ b/app/views/devise/shared/_signin_box.html.haml @@ -1,15 +1,18 @@ -#login-pane.login-box{ role: 'tabpanel', class: 'tab-pane active' } - .login-body - - if form_based_providers.any? - - if crowd_enabled? - %div.tab-pane.active{id: "tab-crowd"} - = render 'devise/sessions/new_crowd' - - @ldap_servers.each_with_index do |server, i| - %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero? && !crowd_enabled?)} - = render 'devise/sessions/new_ldap', server: server - - if signin_enabled? - %div#tab-signin.tab-pane - = render 'devise/sessions/new_base' +- if form_based_providers.any? + - if crowd_enabled? + .login-box.tab-pane.active{id: "crowd", role: 'tabpanel', class: 'tab-pane'} + .login-body + = render 'devise/sessions/new_crowd' + - @ldap_servers.each_with_index do |server, i| + .login-box.tab-pane{id: "#{server['provider_name']}", role: 'tabpanel', class: (:active if i.zero? && !crowd_enabled?)} + .login-body + = render 'devise/sessions/new_ldap', server: server + - if signin_enabled? + .login-box.tab-pane{id: 'ldap-standard', role: 'tabpanel'} + .login-body + = render 'devise/sessions/new_base' - - elsif signin_enabled? +- elsif signin_enabled? + .login-box.tab-pane.active{id: 'login-pane', role: 'tabpanel'} + .login-body = render 'devise/sessions/new_base' diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml index e276e91433a..1e957f0935f 100644 --- a/app/views/devise/shared/_tabs_ldap.html.haml +++ b/app/views/devise/shared/_tabs_ldap.html.haml @@ -1,10 +1,10 @@ -%ul.new-session-tabs.nav-links.nav-tabs +%ul.new-session-tabs.nav-links.nav-tabs{ class: ('custom-provider-tabs' if form_based_providers.any?) } - if crowd_enabled? %li.active - = link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab' + = link_to "Crowd", "#crowd", 'data-toggle' => 'tab' - @ldap_servers.each_with_index do |server, i| %li{class: (:active if i.zero? && !crowd_enabled?)} - = link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab' + = link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab' - if signin_enabled? %li - = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab' + = link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab' diff --git a/app/views/groups/labels/destroy.js.haml b/app/views/groups/labels/destroy.js.haml new file mode 100644 index 00000000000..3dfbfc77c0d --- /dev/null +++ b/app/views/groups/labels/destroy.js.haml @@ -0,0 +1,2 @@ +- if @group.labels.empty? + $('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000) diff --git a/app/views/groups/labels/edit.html.haml b/app/views/groups/labels/edit.html.haml new file mode 100644 index 00000000000..836981fc6fd --- /dev/null +++ b/app/views/groups/labels/edit.html.haml @@ -0,0 +1,7 @@ +- page_title 'Edit', @label.name, 'Labels' + +%h3.page-title + Edit Label +%hr + += render 'shared/labels/form', url: group_label_path(@group, @label), back_path: @previous_labels_path diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml new file mode 100644 index 00000000000..70783a63409 --- /dev/null +++ b/app/views/groups/labels/index.html.haml @@ -0,0 +1,20 @@ +- page_title 'Labels' + +.top-area.adjust + .nav-text + Labels can be applied to issues and merge requests. Group labels are available for any project within the group. + + .nav-controls + - if can?(current_user, :admin_label, @group) + = link_to new_group_label_path(@group), class: "btn btn-new" do + New label + +.labels + .other-labels + - if @labels.present? + %ul.content-list.manage-labels-list.js-other-labels + = render partial: 'shared/label', collection: @labels, as: :label + = paginate @labels, theme: 'gitlab' + - else + .nothing-here-block + No labels created yet. diff --git a/app/views/groups/labels/new.html.haml b/app/views/groups/labels/new.html.haml new file mode 100644 index 00000000000..2be87460b1d --- /dev/null +++ b/app/views/groups/labels/new.html.haml @@ -0,0 +1,8 @@ +- page_title 'New Label' +- header_title group_title(@group, 'Labels', group_labels_path(@group)) + +%h3.page-title + New Label +%hr + += render 'shared/labels/form', url: group_labels_path, back_path: @previous_labels_path diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 27ac1760166..f7edb47b666 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -13,6 +13,10 @@ = link_to activity_group_path(@group), title: 'Activity' do %span Activity + = nav_link(controller: [:group, :labels]) do + = link_to group_labels_path(@group), title: 'Labels' do + %span + Labels = nav_link(controller: [:group, :milestones]) do = link_to group_milestones_path(@group), title: 'Milestones' do %span diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml new file mode 100644 index 00000000000..0995826775a --- /dev/null +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -0,0 +1,177 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +%html{lang: "en"} + %head + %meta{content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/ + %meta{content: "width=device-width, initial-scale=1", name: "viewport"}/ + %meta{content: "IE=edge", "http-equiv" => "X-UA-Compatible"}/ + %title= message.subject + :css + /* CLIENT-SPECIFIC STYLES */ + body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } + table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } + img { -ms-interpolation-mode: bicubic; } + + /* iOS BLUE LINKS */ + a[x-apple-data-detectors] { + color: inherit !important; + text-decoration: none !important; + font-size: inherit !important; + font-family: inherit !important; + font-weight: inherit !important; + line-height: inherit !important; + } + + /* ANDROID MARGIN HACK */ + body { margin:0 !important; } + div[style*="margin: 16px 0"] { margin:0 !important; } + + @media only screen and (max-width: 639px) { + body, #body { + min-width: 320px !important; + } + table.wrapper { + width: 100% !important; + min-width: 320px !important; + } + table.wrapper > tbody > tr > td { + border-left: 0 !important; + border-right: 0 !important; + border-radius: 0 !important; + padding-left: 10px !important; + padding-right: 10px !important; + } + } + %body{style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"} + %table#body{border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;"} + %tbody + %tr.line + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;"}  + %tr.header + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"} + %img{alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55"}/ + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"} + %table.wrapper{border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"} + %table.content{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;"} + %tbody + %tr.alert + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;"} + %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;"} + %img{alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13"}/ + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;"} + Your pipeline has failed. + %tr.spacer + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"} + + %tr.section + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"} + %table.info{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"} Project + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;"} + - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name + - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) + %a.muted{href: namespace_url, style: "color:#333333;text-decoration:none;"} + = namespace_name + \/ + %a.muted{href: project_url(@project), style: "color:#333333;text-decoration:none;"} + = @project.name + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Branch + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} + %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} + %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13"}/ + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} + %a.muted{href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;"} + = @pipeline.ref + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Commit + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} + %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} + %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/ + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} + %a{href: commit_url(@pipeline), style: "color:#3084bb;text-decoration:none;"} + = @pipeline.short_sha + - if @merge_request + in + %a{href: merge_request_url(@merge_request), style: "color:#3084bb;text-decoration:none;"} + = @merge_request.to_reference + .commit{style: "color:#5c5c5c;font-weight:300;"} + = @pipeline.git_commit_message.truncate(50) + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Author + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} + %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %tbody + %tr + - commit = @pipeline.commit + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} + %img.avatar{height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24"}/ + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} + - if commit.author + %a.muted{href: user_url(commit.author), style: "color:#333333;text-decoration:none;"} + = commit.author.name + - else + %span + = commit.author_name + %tr.spacer + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"} + + - failed = @pipeline.statuses.latest.failed + %tr.pre-section + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;"} + Pipeline + %a{href: pipeline_url(@pipeline), style: "color:#3084bb;text-decoration:none;"} + = "\##{@pipeline.id}" + had + = failed.size + failed + = "#{'build'.pluralize(failed.size)}." + %tr.warning + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;"} + Logs may contain sensitive data. Please consider before forwarding this email. + %tr.section + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;"} + %table.builds{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;"} + %tbody + - failed.each do |build| + %tr.build-state + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"} + %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;"} + %img{alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10"}/ + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;"} + = build.stage + %td{align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"} + %a{href: pipeline_build_url(@pipeline, build), style: "color:#3084bb;text-decoration:none;"} + = build.name + %tr.build-log + %td{colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;"} + %pre{style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;"} + = build.trace_html(last_lines: 10).html_safe + %tr.footer + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"} + %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/ + %div + %a{href: profile_notifications_url, style: "color:#3084bb;text-decoration:none;"} Manage all notifications + · + %a{href: help_url, style: "color:#3084bb;text-decoration:none;"} Help + %div + You're receiving this email because of your account on + = succeed "." do + %a{href: root_url, style: "color:#3084bb;text-decoration:none;"}= Gitlab.config.gitlab.host diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb new file mode 100644 index 00000000000..8f8084b58e1 --- /dev/null +++ b/app/views/notify/pipeline_failed_email.text.erb @@ -0,0 +1,31 @@ +Your pipeline has failed. + +Project: <%= @project.name %> ( <%= project_url(@project) %> ) +Branch: <%= @pipeline.ref %> ( <%= commits_url(@pipeline) %> ) +<% if @merge_request -%> +Merge Request: <%= @merge_request.to_reference %> ( <%= merge_request_url(@merge_request) %> ) +<% end -%> + +Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> ) +Commit Message: <%= @pipeline.git_commit_message.truncate(50) %> +<% commit = @pipeline.commit -%> +<% if commit.author -%> +Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> ) +<% else -%> +Commit Author: <%= commit.author_name %> +<% end -%> + +<% failed = @pipeline.statuses.latest.failed -%> +Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>. + +<% failed.each do |build| -%> +Build #<%= build.id %> ( <%= pipeline_build_url(@pipeline, build) %> ) +Stage: <%= build.stage %> +Name: <%= build.name %> +Trace: <%= build.trace_with_state(last_lines: 10)[:text] %> + +<% end -%> + +You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>. +Manage all notifications: <%= profile_notifications_url %> +Help: <%= help_url %> diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml new file mode 100644 index 00000000000..cf9c1d4d72c --- /dev/null +++ b/app/views/notify/pipeline_success_email.html.haml @@ -0,0 +1,154 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +%html{lang: "en"} + %head + %meta{content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/ + %meta{content: "width=device-width, initial-scale=1", name: "viewport"}/ + %meta{content: "IE=edge", "http-equiv" => "X-UA-Compatible"}/ + %title= message.subject + :css + /* CLIENT-SPECIFIC STYLES */ + body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } + table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } + img { -ms-interpolation-mode: bicubic; } + + /* iOS BLUE LINKS */ + a[x-apple-data-detectors] { + color: inherit !important; + text-decoration: none !important; + font-size: inherit !important; + font-family: inherit !important; + font-weight: inherit !important; + line-height: inherit !important; + } + + /* ANDROID MARGIN HACK */ + body { margin:0 !important; } + div[style*="margin: 16px 0"] { margin:0 !important; } + + @media only screen and (max-width: 639px) { + body, #body { + min-width: 320px !important; + } + table.wrapper { + width: 100% !important; + min-width: 320px !important; + } + table.wrapper > tbody > tr > td { + border-left: 0 !important; + border-right: 0 !important; + border-radius: 0 !important; + padding-left: 10px !important; + padding-right: 10px !important; + } + } + %body{style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"} + %table#body{border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;"} + %tbody + %tr.line + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;"}  + %tr.header + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"} + %img{alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55"}/ + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"} + %table.wrapper{border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"} + %table.content{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;"} + %tbody + %tr.success + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;"} + %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;"} + %img{alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13"}/ + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;"} + Your pipeline has passed. + %tr.spacer + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"} + + %tr.section + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"} + %table.info{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"} Project + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;"} + - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name + - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) + %a.muted{href: namespace_url, style: "color:#333333;text-decoration:none;"} + = namespace_name + \/ + %a.muted{href: project_url(@project), style: "color:#333333;text-decoration:none;"} + = @project.name + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Branch + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} + %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} + %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13"}/ + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} + %a.muted{href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;"} + = @pipeline.ref + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Commit + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} + %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %tbody + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} + %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/ + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} + %a{href: commit_url(@pipeline), style: "color:#3084bb;text-decoration:none;"} + = @pipeline.short_sha + - if @merge_request + in + %a{href: merge_request_url(@merge_request), style: "color:#3084bb;text-decoration:none;"} + = @merge_request.to_reference + .commit{style: "color:#5c5c5c;font-weight:300;"} + = @pipeline.git_commit_message.truncate(50) + %tr + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Author + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"} + %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"} + %tbody + %tr + - commit = @pipeline.commit + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} + %img.avatar{height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24"}/ + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} + - if commit.author + %a.muted{href: user_url(commit.author), style: "color:#333333;text-decoration:none;"} + = commit.author.name + - else + %span + = commit.author_name + %tr.spacer + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"} + + %tr.success-message + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;"} + - build_count = @pipeline.statuses.latest.size + - stage_count = @pipeline.stages.size + Pipeline + %a{href: pipeline_url(@pipeline), style: "color:#3084bb;text-decoration:none;"} + = "\##{@pipeline.id}" + successfully completed + = "#{build_count} #{'build'.pluralize(build_count)}" + in + = "#{stage_count} #{'stage'.pluralize(stage_count)}." + %tr.footer + %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"} + %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/ + %div + %a{href: profile_notifications_url, style: "color:#3084bb;text-decoration:none;"} Manage all notifications + · + %a{href: help_url, style: "color:#3084bb;text-decoration:none;"} Help + %div + You're receiving this email because of your account on + = succeed "." do + %a{href: root_url, style: "color:#3084bb;text-decoration:none;"}= Gitlab.config.gitlab.host diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb new file mode 100644 index 00000000000..ae22d474f2c --- /dev/null +++ b/app/views/notify/pipeline_success_email.text.erb @@ -0,0 +1,24 @@ +Your pipeline has passed. + +Project: <%= @project.name %> ( <%= project_url(@project) %> ) +Branch: <%= @pipeline.ref %> ( <%= commits_url(@pipeline) %> ) +<% if @merge_request -%> +Merge Request: <%= @merge_request.to_reference %> ( <%= merge_request_url(@merge_request) %> ) +<% end -%> + +Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> ) +Commit Message: <%= @pipeline.git_commit_message.truncate(50) %> +<% commit = @pipeline.commit -%> +<% if commit.author -%> +Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> ) +<% else -%> +Commit Author: <%= commit.author_name %> +<% end -%> + +<% build_count = @pipeline.statuses.latest.size -%> +<% stage_count = @pipeline.stages.size -%> +Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>. + +You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>. +Manage all notifications: <%= profile_notifications_url %> +Help: <%= help_url %> diff --git a/app/views/projects/_customize_workflow.html.haml b/app/views/projects/_customize_workflow.html.haml new file mode 100644 index 00000000000..d2c1e943db1 --- /dev/null +++ b/app/views/projects/_customize_workflow.html.haml @@ -0,0 +1,8 @@ +.row-content-block.project-home-empty + %div.text-center{ class: container_class } + %h4 + Customize your workflow! + %p + Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and builds, GitLab can help manage your workflow from idea to production! + - if can?(current_user, :admin_project, @project) + = link_to "Get started", edit_project_path(@project), class: "btn btn-success" diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 5590198a20e..d3987fc9c4f 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -22,5 +22,6 @@ = render 'projects/buttons/star' = render 'projects/buttons/fork' - .project-clone-holder - = render "shared/clone_panel" + - if @project.feature_available?(:repository, current_user) + .project-clone-holder + = render "shared/clone_panel" diff --git a/app/views/projects/_wiki.html.haml b/app/views/projects/_wiki.html.haml new file mode 100644 index 00000000000..f00422dd7c0 --- /dev/null +++ b/app/views/projects/_wiki.html.haml @@ -0,0 +1,19 @@ +- if @wiki_home.present? + %div{ class: container_class } + .wiki-holder.prepend-top-default.append-bottom-default + .wiki + = preserve do + = render_wiki_content(@wiki_home) +- else + - can_create_wiki = can?(current_user, :create_wiki, @project) + .project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] } + %div.text-center{ class: container_class } + %h4 + This project does not have a wiki homepage yet + - if can_create_wiki + %p + Add a homepage to your wiki that contains information about your project + %p + We recommend you + = link_to "add a homepage", namespace_project_wiki_path(@project.namespace, @project, :home) + to your project's wiki and GitLab will show it here instead of this message. diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml index d8f16022407..c6d718a1cd1 100644 --- a/app/views/projects/boards/components/_card.html.haml +++ b/app/views/projects/boards/components/_card.html.haml @@ -26,7 +26,7 @@ ":title" => "label.description", data: { container: 'body' } } {{ label.title }} - %a.has-tooltip{ ":href" => "'/' + issue.assignee.username", + %a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username", ":title" => "'Assigned to ' + issue.assignee.name", "v-if" => "issue.assignee", data: { container: 'body' } } diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 966633f1f89..b1053028279 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -1,5 +1,4 @@ -- builds = @build.pipeline.builds.latest.to_a -- statuses = ["failed", "pending", "running", "canceled", "success", "skipped"] +- builds = @build.pipeline.builds.to_a %aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default @@ -124,9 +123,9 @@ %a.stage-item= stage .builds-container - - statuses.each do |build_status| + - HasStatus::ORDERED_STATUSES.each do |build_status| - builds.select{|build| build.status == build_status}.each do |build| - .build-job{class: ('active' if build == @build), data: {stage: build.stage}} + .build-job{class: sidebar_build_class(build, @build), data: {stage: build.stage}} = link_to namespace_project_build_path(@project.namespace, @project, build) do = icon('arrow-right') = ci_icon_for_status(build.status) @@ -135,11 +134,5 @@ = build.name - else = build.id - - - if @build.retried? - %li.active - %a - Build ##{@build.id} - · - %i.fa.fa-warning - This build was retried. + - if build.retried? + %i.fa.fa-refresh.has-tooltip{data: { container: 'body', placement: 'bottom' }, title: 'Build was retried'} diff --git a/app/views/projects/builds/_user.html.haml b/app/views/projects/builds/_user.html.haml index 2642de8021d..83f299da651 100644 --- a/app/views/projects/builds/_user.html.haml +++ b/app/views/projects/builds/_user.html.haml @@ -1,4 +1,7 @@ by %a{ href: user_path(@build.user) } - = image_tag avatar_icon(@build.user, 24), class: "avatar s24" - %strong= @build.user.to_reference + %span.hidden-xs + = image_tag avatar_icon(@build.user, 24), class: "avatar s24" + %strong{ data: { toggle: 'tooltip', placement: 'top', title: @build.user.to_reference } } + = @build.user.name + %strong.visible-xs-inline= @build.user.to_reference diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 9248adfde80..94632056b15 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -6,7 +6,7 @@ - coverage = local_assigns.fetch(:coverage, false) - allow_retry = local_assigns.fetch(:allow_retry, false) -%tr.build.commit +%tr.build.commit{class: ('retried' if retried)} %td.status - if can?(current_user, :read_build, build) = ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build)) @@ -27,7 +27,7 @@ = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name" - else .light none - .icon-container + .icon-container.commit-icon = custom_icon("icon_commit") - if commit_sha @@ -35,8 +35,9 @@ - if build.stuck? = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') + - if retried - = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') + = icon('refresh', class: 'text-warning has-tooltip', title: 'Build was retried') .label-container - if build.tags.any? @@ -47,8 +48,6 @@ %span.label.label-info triggered - if build.try(:allow_failure) %span.label.label-danger allowed to fail - - if retried - %span.label.label-warning retried - if build.manual? %span.label.label-info manual diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml index 017d3ff6af2..55965172d3f 100644 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ b/app/views/projects/ci/builds/_build_pipeline.html.haml @@ -1,10 +1,10 @@ - is_playable = subject.playable? && can?(current_user, :update_build, @project) - if is_playable - = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do + = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } do = render_status_with_link('build', 'play') .ci-status-text= subject.name - elsif can?(current_user, :read_build, @project) - = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do + = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do %span.ci-status-icon = render_status_with_link('build', subject.status) .ci-status-text= subject.name diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 36eadbd2bf1..c6f359f5679 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -9,17 +9,15 @@ = ci_icon_for_status(status) - else = ci_status_with_icon(status) - %td.branch-commit + + %td = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do - %span ##{pipeline.id} - - if pipeline.ref && show_branch - .icon-container - = pipeline.tag? ? icon('tag') : icon('code-fork') - = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name" - - if show_commit - .icon-container - = custom_icon("icon_commit") - = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace" + %span.pipeline-id ##{pipeline.id} + %span by + - if pipeline.user + = user_avatar(user: pipeline.user, size: 20) + - else + %span.api.monospace API - if pipeline.latest? %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest - if pipeline.triggered? @@ -29,6 +27,16 @@ - if pipeline.builds.any?(&:stuck?) %span.label.label-warning stuck + %td.branch-commit + - if pipeline.ref && show_branch + .icon-container + = pipeline.tag? ? icon('tag') : icon('code-fork') + = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name" + - if show_commit + .icon-container.commit-icon + = custom_icon("icon_commit") + = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace" + %p.commit-title - if commit = pipeline.commit = author_avatar(commit, size: 20) @@ -36,16 +44,15 @@ - else Cant find HEAD commit for this branch - - - stages_status = pipeline.statuses.relevant.latest.stages_status - %td.stage-cell - - stages.each do |stage| - - status = stages_status[stage] - - tooltip = "#{stage.titleize}: #{status || 'not found'}" - - if status - .stage-container - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do - = ci_icon_for_status(status) + - stages_status = pipeline.statuses.relevant.latest.stages_status + %td.stage-cell + - stages.each do |stage| + - status = stages_status[stage] + - tooltip = "#{stage.titleize}: #{status || 'not found'}" + - if status + .stage-container + = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do + = ci_icon_for_status(status) %td - if pipeline.duration diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml index 5d0d5ba0262..f2d71fa6989 100644 --- a/app/views/projects/commit/_pipeline_status_group.html.haml +++ b/app/views/projects/commit/_pipeline_status_group.html.haml @@ -1,5 +1,5 @@ - group_status = CommitStatus.where(id: subject).status -%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } +%button.dropdown-menu-toggle.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } } %span.ci-status-icon = render_status_with_link('build', group_status) %span.ci-status-text diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 640651e93f5..ac451441eec 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -8,6 +8,7 @@ %tbody %th Status %th Pipeline + %th Commit %th Stages %th %th diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index 76b68c544aa..7bde20c3286 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -10,7 +10,7 @@ = button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do .dropdown-toggle-text= params[:from] || 'Select branch/tag' = render "ref_dropdown" - .compare-ellipsis ... + .compare-ellipsis.inline ... .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown .input-group.inline-input-group %span.input-group-addon to diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 7f346df8797..b647882efa0 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -2,10 +2,10 @@ - page_title "Cycle Analytics" = render "projects/pipelines/head" -#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project)}} +#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) }} .bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"} - = icon('times', class: 'dismiss-icon', "@click": "dismissLanding()") + = icon('times', class: 'dismiss-icon', "@click" => "dismissLanding()") .row .col-sm-3.col-xs-12.svg-container = custom_icon('icon_cycle_analytics_splash') diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index 22c4a75d213..58a214bdbd1 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -1,28 +1,15 @@ -- if can?(current_user, :create_deployment, deployment) && deployment.deployable - .pull-right - - - external_url = deployment.environment.external_url - - if external_url - = link_to external_url, target: '_blank', class: 'btn external-url' do - = icon('external-link') - - - actions = deployment.manual_actions - - if actions.present? - .inline - .dropdown - %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} - = custom_icon('icon_play') - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - - actions.each do |action| - %li - = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do - = custom_icon('icon_play') - %span= action.name.humanize +- if can?(current_user, :create_deployment, deployment) + - actions = deployment.manual_actions + - if actions.present? + .inline + .dropdown + %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} + = custom_icon('icon_play') + = icon('caret-down') + %ul.dropdown-menu.dropdown-menu-align-right + - actions.each do |action| + %li + = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do + = custom_icon('icon_play') + %span= action.name.humanize - - if local_assigns.fetch(:allow_rollback, false) - = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do - - if deployment.last? - Re-deploy - - else - Rollback diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml index 28813babd7b..ff250eeca50 100644 --- a/app/views/projects/deployments/_commit.html.haml +++ b/app/views/projects/deployments/_commit.html.haml @@ -3,7 +3,7 @@ .icon-container = deployment.tag? ? icon('tag') : icon('code-fork') = link_to deployment.ref, namespace_project_commits_path(@project.namespace, @project, deployment.ref), class: "monospace branch-name" - .icon-container + .icon-container.commit-icon = custom_icon("icon_commit") = link_to deployment.short_sha, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-id monospace" diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index ca0005abd0c..9238f232c7e 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -17,4 +17,6 @@ #{time_ago_with_tooltip(deployment.created_at)} %td.hidden-xs - = render 'projects/deployments/actions', deployment: deployment, allow_rollback: true + .pull-right + = render 'projects/deployments/actions', deployment: deployment + = render 'projects/deployments/rollback', deployment: deployment diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml new file mode 100644 index 00000000000..5941e01c6f1 --- /dev/null +++ b/app/views/projects/deployments/_rollback.haml @@ -0,0 +1,6 @@ +- if can?(current_user, :create_deployment, deployment) && deployment.deployable + = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do + - if deployment.last? + Re-deploy + - else + Rollback diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index c8f84b96cb7..55b6580e640 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -46,62 +46,70 @@ %h5.prepend-top-0 Feature Visibility - = f.fields_for :project_feature do |feature_fields| - .form_group.prepend-top-20 - .row - .col-md-9 - = feature_fields.label :issues_access_level, "Issues", class: 'label-light' - %span.help-block Lightweight issue tracking system for this project - .col-md-3 - = project_feature_access_select(:issues_access_level) + = f.fields_for :project_feature do |feature_fields| + .form_group.prepend-top-20 + .row + .col-md-9 + = feature_fields.label :repository_access_level, "Repository", class: 'label-light' + %span.help-block Push files to be stored in this project + .col-md-3.js-repo-access-level + = project_feature_access_select(:repository_access_level) - .row - .col-md-9 - = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' - %span.help-block Submit changes to be merged upstream - .col-md-3 - = project_feature_access_select(:merge_requests_access_level) + .col-sm-12 + .row + .col-md-9.project-feature-nested + = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' + %span.help-block Submit changes to be merged upstream + .col-md-3 + = project_feature_access_select(:merge_requests_access_level) - .row - .col-md-9 - = feature_fields.label :builds_access_level, "Builds", class: 'label-light' - %span.help-block Submit Test and deploy your changes before merge - .col-md-3 - = project_feature_access_select(:builds_access_level) + .row + .col-md-9.project-feature-nested + = feature_fields.label :builds_access_level, "Builds", class: 'label-light' + %span.help-block Submit, test and deploy your changes before merge + .col-md-3 + = project_feature_access_select(:builds_access_level) - .row - .col-md-9 - = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' - %span.help-block Pages for project documentation - .col-md-3 - = project_feature_access_select(:wiki_access_level) + .row + .col-md-9 + = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' + %span.help-block Share code pastes with others out of Git repository + .col-md-3 + = project_feature_access_select(:snippets_access_level) - .row - .col-md-9 - = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' - %span.help-block Share code pastes with others out of Git repository - .col-md-3 - = project_feature_access_select(:snippets_access_level) + .row + .col-md-9 + = feature_fields.label :issues_access_level, "Issues", class: 'label-light' + %span.help-block Lightweight issue tracking system for this project + .col-md-3 + = project_feature_access_select(:issues_access_level) - - if Gitlab.config.lfs.enabled && current_user.admin? .row .col-md-9 - = f.label :lfs_enabled, 'LFS', class: 'label-light' - %span.help-block - Git Large File Storage - = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' + %span.help-block Pages for project documentation .col-md-3 - = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control', data: { field: 'lfs_enabled' } + = project_feature_access_select(:wiki_access_level) - - if Gitlab.config.registry.enabled - .form-group - .checkbox - = f.label :container_registry_enabled do - = f.check_box :container_registry_enabled - %strong Container Registry - %br - %span.descr Enable Container Registry for this project - = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank' + - if Gitlab.config.lfs.enabled && current_user.admin? + .checkbox + = f.label :lfs_enabled do + = f.check_box :lfs_enabled + %strong LFS + %br + %span.descr + Git Large File Storage + = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + + - if Gitlab.config.lfs.enabled && current_user.admin? + .form-group + .checkbox + = f.label :container_registry_enabled do + = f.check_box :container_registry_enabled + %strong Container Registry + %br + %span.descr Enable Container Registry for this project + = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank' = render 'merge_request_settings', f: f %hr @@ -280,4 +288,4 @@ Saving project. %p Please wait a moment, this page will automatically refresh when ready. -= render 'shared/confirm_modal', phrase: @project.path += render 'shared/confirm_modal', phrase: @project.path
\ No newline at end of file diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml index 251694e897c..b75d5df4150 100644 --- a/app/views/projects/environments/_environment.html.haml +++ b/app/views/projects/environments/_environment.html.haml @@ -28,4 +28,8 @@ #{time_ago_with_tooltip(last_deployment.created_at)} %td.hidden-xs - = render 'projects/deployments/actions', deployment: last_deployment + .pull-right + = render 'projects/environments/external_url', environment: environment + = render 'projects/deployments/actions', deployment: last_deployment + = render 'projects/environments/stop', environment: environment + = render 'projects/deployments/rollback', deployment: last_deployment diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml new file mode 100644 index 00000000000..4c8fe1c271b --- /dev/null +++ b/app/views/projects/environments/_external_url.html.haml @@ -0,0 +1,3 @@ +- if environment.external_url && can?(current_user, :read_environment, environment) + = link_to environment.external_url, target: '_blank', class: 'btn external-url' do + = icon('external-link') diff --git a/app/views/projects/environments/_stop.html.haml b/app/views/projects/environments/_stop.html.haml new file mode 100644 index 00000000000..69848123c17 --- /dev/null +++ b/app/views/projects/environments/_stop.html.haml @@ -0,0 +1,5 @@ +- if can?(current_user, :create_deployment, environment) && environment.stoppable? + .inline + = link_to stop_namespace_project_environment_path(@project.namespace, @project, environment), method: :post, + class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do + = icon('stop', class: 'stop-env-icon') diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 721ba156334..8f555afcf11 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -3,14 +3,27 @@ = render "projects/pipelines/head" %div{ class: container_class } - - if can?(current_user, :create_environment, @project) && !@environments.blank? - .top-area + .top-area + %ul.nav-links + %li{class: ('active' if @scope.nil?)} + = link_to project_environments_path(@project) do + Available + %span.badge.js-available-environments-count + = number_with_delimiter(@all_environments.available.count) + + %li{class: ('active' if @scope == 'stopped')} + = link_to project_environments_path(@project, scope: :stopped) do + Stopped + %span.badge.js-stopped-environments-count + = number_with_delimiter(@all_environments.stopped.count) + + - if can?(current_user, :create_environment, @project) && !@all_environments.blank? .nav-controls = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do New environment .environments-container - - if @environments.blank? + - if @all_environments.blank? .blank-state.blank-state-no-icon %h2.blank-state-title You don't have any environments right now. diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 90c59223a35..bcac73d3698 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -3,14 +3,16 @@ = render "projects/pipelines/head" %div{ class: container_class } - .top-area + .top-area.adjust .col-md-9 %h3.page-title= @environment.name.capitalize .col-md-3 .nav-controls + = render 'projects/environments/external_url', environment: @environment - if can?(current_user, :update_environment, @environment) = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' - = link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete + - if can?(current_user, :create_deployment, @environment) && @environment.stoppable? + = link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post .deployments-container - if @deployments.blank? diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml index 0a66d60accc..c45b73e4225 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml @@ -1,9 +1,10 @@ -- if subject.target_url - = link_to subject.target_url do +%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } } + - if subject.target_url + = link_to subject.target_url do + %span.ci-status-icon + = render_status_with_link('commit status', subject.status) + %span.ci-status-text= subject.name + - else %span.ci-status-icon = render_status_with_link('commit status', subject.status) %span.ci-status-text= subject.name -- else - %span.ci-status-icon - = render_status_with_link('commit status', subject.status) - %span.ci-status-text= subject.name diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 8b1a8a8a2d9..c80210d6ff4 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -50,7 +50,7 @@ - if issue.labels.any? - issue.labels.each do |label| - = link_to_label(label, project: issue.project) + = link_to_label(label, subject: issue.project) - if issue.tasks? %span.task-status diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml index a2c31c0b4c5..a4b752ad86d 100644 --- a/app/views/projects/issues/_issues.html.haml +++ b/app/views/projects/issues/_issues.html.haml @@ -1,5 +1,5 @@ %ul.content-list.issues-list.issuable-list - = render @issues + = render partial: "projects/issues/issue", collection: @issues - if @issues.blank? %li .nothing-here-block No issues to show diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml index 3a6fbbc7fbc..1b7d878c38c 100644 --- a/app/views/projects/issues/edit.html.haml +++ b/app/views/projects/issues/edit.html.haml @@ -1,4 +1,4 @@ -- page_title "Edit", "#{@issue.to_reference} #{@issue.title}", "Issues" +- page_title "Edit", "#{@issue.title} (#{@issue.to_reference})", "Issues" %h3.page-title Edit Issue ##{@issue.iid} diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 09347ad5fff..bd629b5c519 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,4 +1,4 @@ -- page_title "#{@issue.to_reference} #{@issue.title}", "Issues" +- page_title "#{@issue.title} (#{@issue.to_reference})", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes @@ -53,7 +53,7 @@ .issue-details.issuable-details - .detail-page-description.content-block + .detail-page-description.content-block{ class: ('hide-bottom-border' unless @issue.description.present? ) } %h2.title = markdown_field(@issue, :title) - if @issue.description.present? diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml deleted file mode 100644 index 71f7f354d72..00000000000 --- a/app/views/projects/labels/_label.html.haml +++ /dev/null @@ -1,50 +0,0 @@ -- label_css_id = dom_id(label) -%li{id: label_css_id, data: { id: label.id } } - = render "shared/label_row", label: label - - .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown - %button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } } - Options - = icon('caret-down') - .dropdown-menu.dropdown-menu-align-right - %ul - %li - = link_to_label(label, type: :merge_request) do - = pluralize label.open_merge_requests_count, 'merge request' - %li - = link_to_label(label) do - = pluralize label.open_issues_count(current_user), 'open issue' - - if current_user - %li.label-subscription{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } - %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } } - %span= label_subscription_toggle_button_text(label) - - if can? current_user, :admin_label, @project - %li - = link_to "Edit", edit_namespace_project_label_path(@project.namespace, @project, label) - %li - = link_to "Delete", namespace_project_label_path(@project.namespace, @project, label), title: "Delete", method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} - - .pull-right.hidden-xs.hidden-sm.hidden-md - = link_to_label(label, type: :merge_request, css_class: 'btn btn-transparent btn-action') do - = pluralize label.open_merge_requests_count, 'merge request' - = link_to_label(label, css_class: 'btn btn-transparent btn-action') do - = pluralize label.open_issues_count(current_user), 'open issue' - - - if current_user - .label-subscription.inline{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } - %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } } - %span.sr-only= label_subscription_toggle_button_text(label) - = icon('eye', class: 'label-subscribe-button-icon') - = icon('spinner spin', class: 'label-subscribe-button-loading') - - - if can? current_user, :admin_label, @project - = link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do - %span.sr-only Edit - = icon('pencil-square-o') - = link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?", toggle: "tooltip"} do - %span.sr-only Delete - = icon('trash-o') - - - if current_user - :javascript - new Subscription('##{dom_id(label)} .label-subscription'); diff --git a/app/views/projects/labels/destroy.js.haml b/app/views/projects/labels/destroy.js.haml index d59563b122a..8d09e2bda11 100644 --- a/app/views/projects/labels/destroy.js.haml +++ b/app/views/projects/labels/destroy.js.haml @@ -1,2 +1,2 @@ -- if @project.labels.size == 0 +- if @labels.empty? $('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000) diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml index 52b187e7e58..a80a07b52e6 100644 --- a/app/views/projects/labels/edit.html.haml +++ b/app/views/projects/labels/edit.html.haml @@ -6,4 +6,4 @@ %h3.page-title Edit Label %hr - = render 'form' + = render 'shared/labels/form', url: namespace_project_label_path(@project.namespace.becomes(Namespace), @project, @label), back_path: namespace_project_labels_path(@project.namespace, @project) diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index db66a0edbd8..f135bf6f6b4 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -16,21 +16,22 @@ .labels - if can?(current_user, :admin_label, @project) -# Only show it in the first page - - hide = @project.labels.empty? || (params[:page].present? && params[:page] != '1') + - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') .prioritized-labels{ class: ('hide' if hide) } %h5 Prioritized Labels %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) } %p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet - if @prioritized_labels.present? - = render @prioritized_labels + = render partial: 'shared/label', collection: @prioritized_labels, as: :label + .other-labels - if can?(current_user, :admin_label, @project) %h5{ class: ('hide' if hide) } Other Labels - - if @labels.present? - %ul.content-list.manage-labels-list.js-other-labels - = render @labels + %ul.content-list.manage-labels-list.js-other-labels + - if @labels.present? + = render partial: 'shared/label', collection: @labels, as: :label = paginate @labels, theme: 'gitlab' - - else + - if @labels.blank? .nothing-here-block - if can?(current_user, :admin_label, @project) Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}. diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml index a1bb66cfb6c..f0d9be744d1 100644 --- a/app/views/projects/labels/new.html.haml +++ b/app/views/projects/labels/new.html.haml @@ -6,4 +6,4 @@ %h3.page-title New Label %hr - = render 'form' + = render 'shared/labels/form', url: namespace_project_labels_path(@project.namespace.becomes(Namespace), @project), back_path: namespace_project_labels_path(@project.namespace, @project) diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 68fb7d5a414..12408068834 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -62,7 +62,7 @@ - if merge_request.labels.any? - merge_request.labels.each do |label| - = link_to_label(label, project: merge_request.project, type: 'merge_request') + = link_to_label(label, subject: merge_request.project, type: :merge_request) - if merge_request.tasks? %span.task-status diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index da6927879a4..9c6f562f7db 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -29,7 +29,11 @@ = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do Commits %span.badge= @commits.size - - if @pipeline + - if @pipelines.any? + %li.builds-tab + = link_to url_for(params), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do + Pipelines + %span.badge= @pipelines.size %li.builds-tab = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do Builds @@ -44,9 +48,11 @@ = render "projects/merge_requests/show/commits" #diffs.diffs.tab-pane - # This tab is always loaded via AJAX - - if @pipeline + - if @pipelines.any? #builds.builds.tab-pane = render "projects/merge_requests/show/builds" + #pipelines.pipelines.tab-pane + = render "projects/merge_requests/show/pipelines" .mr-loading-status = spinner @@ -59,5 +65,5 @@ :javascript var merge_request = new MergeRequest({ action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}", - buildsLoaded: "#{@pipeline ? 'true' : 'false'}" + buildsLoaded: "#{@pipelines.any? ? 'true' : 'false'}" }); diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 662463bc72b..f57abe73977 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,4 +1,4 @@ -- page_title "#{@merge_request.to_reference} #{@merge_request.title}", "Merge Requests" +- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - content_for :page_specific_javascripts do @@ -26,19 +26,19 @@ %ul.dropdown-menu.dropdown-menu-align-right %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) - - unless @merge_request.closed_without_fork? - .normal - %span Request to merge - %span.label-branch= source_branch_with_namespace(@merge_request) - %span into - %span.label-branch - = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) - - if @merge_request.open? && @merge_request.diverged_from_target_branch? - %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind) + .normal + %span Request to merge + %span.label-branch= source_branch_with_namespace(@merge_request) + %span into + %span.label-branch + = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) + - if @merge_request.open? && @merge_request.diverged_from_target_branch? + %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind) - - unless @merge_request.closed_without_source_project? + - if @merge_request.source_branch_exists? = render "projects/merge_requests/show/how_to_merge" - = render "projects/merge_requests/widget/show.html.haml" + + = render "projects/merge_requests/widget/show.html.haml" - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user) .light.prepend-top-default.append-bottom-default @@ -47,39 +47,41 @@ = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - if @commits_count.nonzero? - %ul.merge-request-tabs.nav-links.no-top.no-bottom{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } - %li.notes-tab - = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do - Discussion - %span.badge= @merge_request.mr_and_commit_notes.user.count - - unless @merge_request.closed_without_source_project? - %li.commits-tab - = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do - Commits - %span.badge= @commits_count - - if @pipeline - %li.pipelines-tab - = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do - Pipelines - %span.badge= @merge_request.all_pipelines.size - %li.builds-tab - = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do - Builds - %span.badge= @statuses.size - %li.diffs-tab - = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do - Changes - %span.badge= @merge_request.diff_size - %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true } - %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" } - .line-resolve-all{ "v-show" => "discussionCount > 0", - ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } - %span.line-resolve-btn.is-disabled{ type: "button", - ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } - = render "shared/icons/icon_status_success.svg" - %span.line-resolve-text - {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ discussionCount | pluralize 'discussion' }} resolved - = render "discussions/jump_to_next" + .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } + %div{ class: container_class } + %ul.merge-request-tabs.nav-links.no-top.no-bottom + %li.notes-tab + = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do + Discussion + %span.badge= @merge_request.mr_and_commit_notes.user.count + - if @merge_request.source_project + %li.commits-tab + = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do + Commits + %span.badge= @commits_count + - if @pipeline + %li.pipelines-tab + = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do + Pipelines + %span.badge= @pipelines.size + %li.builds-tab + = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do + Builds + %span.badge= @statuses.size + %li.diffs-tab + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do + Changes + %span.badge= @merge_request.diff_size + %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true } + %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" } + .line-resolve-all{ "v-show" => "discussionCount > 0", + ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } + %span.line-resolve-btn.is-disabled{ type: "button", + ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } + = render "shared/icons/icon_status_success.svg" + %span.line-resolve-text + {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ discussionCount | pluralize 'discussion' }} resolved + = render "discussions/jump_to_next" .tab-content#diff-notes-app #notes.notes.tab-pane.voting_notes diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml index 7c3ac6652ee..03159f123f3 100644 --- a/app/views/projects/merge_requests/edit.html.haml +++ b/app/views/projects/merge_requests/edit.html.haml @@ -1,4 +1,4 @@ -- page_title "Edit", "#{@merge_request.to_reference} #{@merge_request.title}", "Merge Requests" +- page_title "Edit", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" %h3.page-title Edit Merge Request #{@merge_request.to_reference} diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index c83818e9199..f9ba77e87b5 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -31,7 +31,7 @@ = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do Delete - .detail-page-description.milestone-detail + .detail-page-description.milestone-detail{ class: ('hide-bottom-border' unless @milestone.description.present? ) } %h2.title = markdown_field(@milestone, :title) %div diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index cc8cb134fb8..932603f03b0 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -27,6 +27,7 @@ - else .input-group-addon.static-namespace #{root_url}#{current_user.username}/ + = f.hidden_field :namespace_id, value: current_user.namespace_id .form-group.col-xs-12.col-sm-6.project-path = f.label :namespace_id, class: 'label-light' do %span @@ -126,6 +127,11 @@ } }); + $('#new_project').submit(function(){ + var $path = $('#project_path'); + $path.val($path.val().trim()); + }); + $('#project_path').keyup(function(){ if($(this).val().length !=0) { $('.btn_import_gitlab_project').attr('disabled', false); diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 73fe6a715fa..ab719e38904 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -57,7 +57,7 @@ = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = icon('pencil', class: 'link-highlight') = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do - = icon('trash-o') + = icon('trash-o', class: 'danger-highlight') .note-body{class: note_editable ? 'js-task-list-container' : ''} .note-text.md = preserve do diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 9eeef5f57b4..4bc49072f35 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -45,11 +45,12 @@ .table-holder %table.table.ci-table %thead - %th.col-xs-1.col-sm-1 Status - %th.col-xs-2.col-sm-4 Pipeline - %th.col-xs-2.col-sm-2 Stages - %th.col-xs-2.col-sm-2 - %th.hidden-xs.col-sm-3 + %th Status + %th Pipeline + %th Commit + %th Stages + %th + %th.hidden-xs = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages = paginate @pipelines, theme: 'gitlab' diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml index 8c7222bfe3d..bebf0ccd54d 100644 --- a/app/views/projects/pipelines_settings/show.html.haml +++ b/app/views/projects/pipelines_settings/show.html.haml @@ -7,7 +7,7 @@ .col-lg-9 %h5.prepend-top-0 Pipelines - = form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project), remote: true, authenticity_token: true do |f| + = form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project) do |f| %fieldset.builds-feature - unless @repository.gitlab_ci_yml .form-group @@ -64,8 +64,8 @@ .checkbox = f.label :public_builds do = f.check_box :public_builds - %strong Public pipelines - .help-block Allow everyone to access pipelines for Public and Internal projects + %strong Public builds + .help-block Allow everyone to access builds traces for Public and Internal projects .form-group.append-bottom-default = f.label :runners_token, "Runners token", class: 'label-light' diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 49dcc9a6ba4..42e9bdbd30e 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -1,4 +1,6 @@ - page_title "Protected branches" +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('protected_branches/protected_branches_bundle.js') .row.prepend-top-default.append-bottom-default .col-lg-3 diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index 858af78f7bf..51b0939564e 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -26,4 +26,4 @@ %h4.underlined-title Available specific runners %ul.bordered-list.available-specific-runners = render partial: 'runner', collection: @assignable_runners, as: :runner - = paginate @assignable_runners + = paginate @assignable_runners, theme: "gitlab" diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index ea4deb6cb28..ba16c641462 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -12,72 +12,74 @@ = render 'projects/last_push' = render "home_panel" -%nav.project-stats{ class: (container_class) } - %ul.nav - %li - = link_to project_files_path(@project) do - Files (#{repository_size}) - %li - = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do - #{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)}) - %li - = link_to namespace_project_branches_path(@project.namespace, @project) do - #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) - %li - = link_to namespace_project_tags_path(@project.namespace, @project) do - #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)}) - - - if default_project_view != 'readme' && @repository.readme +- if @project.feature_available?(:repository, current_user) + %nav.project-stats{ class: container_class } + %ul.nav %li - = link_to 'Readme', readme_path(@project) - - - if @repository.changelog + = link_to project_files_path(@project) do + Files (#{repository_size}) %li - = link_to 'Changelog', changelog_path(@project) - - - if @repository.license_blob + = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do + #{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)}) %li - = link_to license_short_name(@project), license_path(@project) - - - if @repository.contribution_guide + = link_to namespace_project_branches_path(@project.namespace, @project) do + #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) %li - = link_to 'Contribution guide', contribution_guide_path(@project) + = link_to namespace_project_tags_path(@project.namespace, @project) do + #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)}) - - if @repository.gitlab_ci_yml - %li - = link_to 'CI configuration', ci_configuration_path(@project) - - - if current_user && can_push_branch?(@project, @project.default_branch) - - unless @repository.changelog - %li.missing - = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do - Add Changelog - - unless @repository.license_blob - %li.missing - = link_to add_special_file_path(@project, file_name: 'LICENSE') do - Add License - - unless @repository.contribution_guide - %li.missing - = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do - Add Contribution guide - - unless @repository.gitlab_ci_yml - %li.missing - = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do - Set Up CI - - %li.project-repo-buttons-right - .project-repo-buttons.project-right-buttons - - if current_user - = render 'shared/members/access_request_buttons', source: @project - = render "projects/buttons/koding" - - = render 'projects/buttons/download', project: @project, ref: @ref - = render 'projects/buttons/dropdown' - - = render 'shared/notifications/button', notification_setting: @notification_setting -- if @repository.commit - .project-last-commit{ class: container_class } - = render 'projects/last_commit', commit: @repository.commit, project: @project + - if default_project_view != 'readme' && @repository.readme + %li + = link_to 'Readme', readme_path(@project) + + - if @repository.changelog + %li + = link_to 'Changelog', changelog_path(@project) + + - if @repository.license_blob + %li + = link_to license_short_name(@project), license_path(@project) + + - if @repository.contribution_guide + %li + = link_to 'Contribution guide', contribution_guide_path(@project) + + - if @repository.gitlab_ci_yml + %li + = link_to 'CI configuration', ci_configuration_path(@project) + + - if current_user && can_push_branch?(@project, @project.default_branch) + - unless @repository.changelog + %li.missing + = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do + Add Changelog + - unless @repository.license_blob + %li.missing + = link_to add_special_file_path(@project, file_name: 'LICENSE') do + Add License + - unless @repository.contribution_guide + %li.missing + = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do + Add Contribution guide + - unless @repository.gitlab_ci_yml + %li.missing + = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do + Set Up CI + + %li.project-repo-buttons-right + .project-repo-buttons.project-right-buttons + - if current_user + = render 'shared/members/access_request_buttons', source: @project + = render "projects/buttons/koding" + + .btn-group.project-repo-btn-group + = render 'projects/buttons/download', project: @project, ref: @ref + = render 'projects/buttons/dropdown' + + = render 'shared/notifications/button', notification_setting: @notification_setting + - if @repository.commit + .project-last-commit{ class: container_class } + = render 'projects/last_commit', commit: @repository.commit, project: @project %div{ class: container_class } - if @project.archived? @@ -86,5 +88,7 @@ = icon("exclamation-triangle fw") Archived project! Repository is read-only - %div{class: "project-show-#{default_project_view}"} - = render default_project_view + - view_path = default_project_view + + %div{ class: project_child_container_class(view_path) } + = render view_path diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml new file mode 100644 index 00000000000..40c8d2af226 --- /dev/null +++ b/app/views/shared/_label.html.haml @@ -0,0 +1,53 @@ +- label_css_id = dom_id(label) +- open_issues_count = label.open_issues_count(current_user, @project) +- open_merge_requests_count = label.open_merge_requests_count(current_user, @project) + +%li{id: label_css_id, data: { id: label.id } } + = render "shared/label_row", label: label + + .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown + %button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } } + Options + = icon('caret-down') + .dropdown-menu.dropdown-menu-align-right + %ul + %li + = link_to_label(label, subject: @project, type: :merge_request) do + = pluralize open_merge_requests_count, 'merge request' + %li + = link_to_label(label, subject: @project) do + = pluralize open_issues_count, 'open issue' + - if current_user + %li.label-subscription{ data: toggle_subscription_data(label) } + %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } } + %span= label_subscription_toggle_button_text(label) + - if can?(current_user, :admin_label, label) + %li + = link_to 'Edit', edit_label_path(label) + %li + = link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, remote: true, data: {confirm: 'Remove this label? Are you sure?'} + + .pull-right.hidden-xs.hidden-sm.hidden-md + = link_to_label(label, subject: @project, type: :merge_request, css_class: 'btn btn-transparent btn-action') do + = pluralize open_merge_requests_count, 'merge request' + = link_to_label(label, subject: @project, css_class: 'btn btn-transparent btn-action') do + = pluralize open_issues_count, 'open issue' + + - if current_user + .label-subscription.inline{ data: toggle_subscription_data(label) } + %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } } + %span.sr-only= label_subscription_toggle_button_text(label) + = icon('eye', class: 'label-subscribe-button-icon', disabled: label.is_a?(GroupLabel)) + = icon('spinner spin', class: 'label-subscribe-button-loading') + + - if can?(current_user, :admin_label, label) + = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do + %span.sr-only Edit + = icon('pencil-square-o') + = link_to destroy_label_path(label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: label_deletion_confirm_text(label), toggle: "tooltip"} do + %span.sr-only Delete + = icon('trash-o') + + - if current_user && label.is_a?(ProjectLabel) + :javascript + new Subscription('##{dom_id(label)} .label-subscription'); diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index 6f593e8dff9..d28f9421ecf 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -3,13 +3,16 @@ .draggable-handler = icon('bars') .js-toggle-priority.toggle-priority{ data: { url: remove_priority_namespace_project_label_path(@project.namespace, @project, label), - dom_id: dom_id(label) } } + dom_id: dom_id(label), type: label.type } } %button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' } = icon('star-o') %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', :'data-placement' => 'top' } = icon('star') %span.label-name - = link_to_label(label, tooltip: false) + = link_to_label(label, subject: @project, tooltip: false) + - if defined?(@project) && @project.group.present? + %span.label-type + = label.model_name.human.titleize - if label.description %span.label-description = markdown_field(label, :description) diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml index e324d0e5203..21b37a7c9ae 100644 --- a/app/views/shared/_labels_row.html.haml +++ b/app/views/shared/_labels_row.html.haml @@ -1,5 +1,5 @@ - labels.each do |label| %span.label-row.btn-group{ role: "group", aria: { label: label.name }, style: "color: #{text_color_for_bg(label.color)}" } - = link_to_label(label, css_class: 'btn btn-transparent') + = link_to_label(label, subject: @project, css_class: 'btn btn-transparent') %button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } } = icon("times") diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 31620297be0..ed93857e6d4 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -29,8 +29,9 @@ .filter-item.inline.labels-filter = render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" } - .filter-item.inline.reset-filters - %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters + - if issuable_filters_present + .filter-item.inline.reset-filters + %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters .pull-right - if boards_page @@ -77,11 +78,10 @@ = hidden_field_tag :state_event, params[:state_event] .filter-item.inline = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save" - - - if !@labels.nil? - .row-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) } - - if @labels.any? - = render "shared/labels_row", labels: @labels + - has_labels = @labels && @labels.any? + .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) } + - if has_labels + = render 'shared/labels_row', labels: @labels :javascript new UsersSelect(); diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index a7944a60130..d410755cad1 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -88,19 +88,19 @@ - if issuable.assignee_id = f.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", show_menu_above: true } }) + 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}" .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_menu_above: true, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" + = 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 = issuable.project.labels.any? + - 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: '' .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 has_due_date .col-lg-6 .form-group diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index ba9f0c27661..7363ead09ff 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -107,7 +107,7 @@ = dropdown_content do .js-due-date-calendar - - if issuable.project.labels.any? + - if @labels && @labels.any? - selected_labels = issuable.labels .block.labels .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } } diff --git a/app/views/projects/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml index 6ab6ae50389..647e05e5ff7 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/shared/labels/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f| += form_for @label, as: :label, url: url, html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f| = form_errors(@label) .form-group @@ -30,4 +30,4 @@ = f.submit 'Save changes', class: 'btn btn-save js-save-button' - else = f.submit 'Create Label', class: 'btn btn-create js-save-button' - = link_to "Cancel", namespace_project_labels_path(@project.namespace, @project), class: 'btn btn-cancel' + = link_to 'Cancel', back_path, class: 'btn btn-cancel' diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb index 667fff031dd..c2dc955b27c 100644 --- a/app/workers/admin_email_worker.rb +++ b/app/workers/admin_email_worker.rb @@ -1,7 +1,6 @@ class AdminEmailWorker include Sidekiq::Worker - - sidekiq_options retry: false # this job auto-repeats via sidekiq-cron + include CronjobQueue def perform repository_check_failed_count = Project.where(last_repository_check_failed: true).count diff --git a/app/workers/build_coverage_worker.rb b/app/workers/build_coverage_worker.rb index 0680645a8db..def0ab1dde1 100644 --- a/app/workers/build_coverage_worker.rb +++ b/app/workers/build_coverage_worker.rb @@ -1,6 +1,6 @@ class BuildCoverageWorker include Sidekiq::Worker - sidekiq_options queue: :default + include BuildQueue def perform(build_id) Ci::Build.find_by(id: build_id) diff --git a/app/workers/build_email_worker.rb b/app/workers/build_email_worker.rb index 1c7a04a66a8..5fdb1f2baa0 100644 --- a/app/workers/build_email_worker.rb +++ b/app/workers/build_email_worker.rb @@ -1,5 +1,6 @@ class BuildEmailWorker include Sidekiq::Worker + include BuildQueue def perform(build_id, recipients, push_data) recipients.each do |recipient| diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb index e7286b77ac5..466410bf08c 100644 --- a/app/workers/build_finished_worker.rb +++ b/app/workers/build_finished_worker.rb @@ -1,5 +1,6 @@ class BuildFinishedWorker include Sidekiq::Worker + include BuildQueue def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb index e22ececb3fd..9965af935d4 100644 --- a/app/workers/build_hooks_worker.rb +++ b/app/workers/build_hooks_worker.rb @@ -1,6 +1,6 @@ class BuildHooksWorker include Sidekiq::Worker - sidekiq_options queue: :default + include BuildQueue def perform(build_id) Ci::Build.find_by(id: build_id) diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb index 500d357ce31..e0ad5268664 100644 --- a/app/workers/build_success_worker.rb +++ b/app/workers/build_success_worker.rb @@ -1,6 +1,6 @@ class BuildSuccessWorker include Sidekiq::Worker - sidekiq_options queue: :default + include BuildQueue def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| diff --git a/app/workers/clear_database_cache_worker.rb b/app/workers/clear_database_cache_worker.rb index c541daba50e..c4cb4733482 100644 --- a/app/workers/clear_database_cache_worker.rb +++ b/app/workers/clear_database_cache_worker.rb @@ -1,6 +1,7 @@ # This worker clears all cache fields in the database, working in batches. class ClearDatabaseCacheWorker include Sidekiq::Worker + include DedicatedSidekiqQueue BATCH_SIZE = 1000 diff --git a/app/workers/concerns/build_queue.rb b/app/workers/concerns/build_queue.rb new file mode 100644 index 00000000000..cf0ead40a8b --- /dev/null +++ b/app/workers/concerns/build_queue.rb @@ -0,0 +1,8 @@ +# Concern for setting Sidekiq settings for the various CI build workers. +module BuildQueue + extend ActiveSupport::Concern + + included do + sidekiq_options queue: :build + end +end diff --git a/app/workers/concerns/cronjob_queue.rb b/app/workers/concerns/cronjob_queue.rb new file mode 100644 index 00000000000..e918bb011e0 --- /dev/null +++ b/app/workers/concerns/cronjob_queue.rb @@ -0,0 +1,9 @@ +# Concern that sets various Sidekiq settings for workers executed using a +# cronjob. +module CronjobQueue + extend ActiveSupport::Concern + + included do + sidekiq_options queue: :cronjob, retry: false + end +end diff --git a/app/workers/concerns/dedicated_sidekiq_queue.rb b/app/workers/concerns/dedicated_sidekiq_queue.rb new file mode 100644 index 00000000000..132bae6022b --- /dev/null +++ b/app/workers/concerns/dedicated_sidekiq_queue.rb @@ -0,0 +1,9 @@ +# Concern that sets the queue of a Sidekiq worker based on the worker's class +# name/namespace. +module DedicatedSidekiqQueue + extend ActiveSupport::Concern + + included do + sidekiq_options queue: name.sub(/Worker\z/, '').underscore.tr('/', '_') + end +end diff --git a/app/workers/concerns/pipeline_queue.rb b/app/workers/concerns/pipeline_queue.rb new file mode 100644 index 00000000000..ca3860e1d38 --- /dev/null +++ b/app/workers/concerns/pipeline_queue.rb @@ -0,0 +1,8 @@ +# Concern for setting Sidekiq settings for the various CI pipeline workers. +module PipelineQueue + extend ActiveSupport::Concern + + included do + sidekiq_options queue: :pipeline + end +end diff --git a/app/workers/concerns/repository_check_queue.rb b/app/workers/concerns/repository_check_queue.rb new file mode 100644 index 00000000000..a597321ccf4 --- /dev/null +++ b/app/workers/concerns/repository_check_queue.rb @@ -0,0 +1,8 @@ +# Concern for setting Sidekiq settings for the various repository check workers. +module RepositoryCheckQueue + extend ActiveSupport::Concern + + included do + sidekiq_options queue: :repository_check, retry: false + end +end diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb index 6ff361e4d80..3194c389b3d 100644 --- a/app/workers/delete_user_worker.rb +++ b/app/workers/delete_user_worker.rb @@ -1,5 +1,6 @@ class DeleteUserWorker include Sidekiq::Worker + include DedicatedSidekiqQueue def perform(current_user_id, delete_user_id, options = {}) delete_user = User.find(delete_user_id) diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb index 842eebdea9e..d3f7e479a8d 100644 --- a/app/workers/email_receiver_worker.rb +++ b/app/workers/email_receiver_worker.rb @@ -1,7 +1,6 @@ class EmailReceiverWorker include Sidekiq::Worker - - sidekiq_options queue: :incoming_email + include DedicatedSidekiqQueue def perform(raw) return unless Gitlab::IncomingEmail.enabled? diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 1dc7e0adef7..b9cd49985dc 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -1,7 +1,7 @@ class EmailsOnPushWorker include Sidekiq::Worker + include DedicatedSidekiqQueue - sidekiq_options queue: :mailers attr_reader :email, :skip_premailer def perform(project_id, recipients, push_data, options = {}) diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb index 174eabff9fd..a27585fd389 100644 --- a/app/workers/expire_build_artifacts_worker.rb +++ b/app/workers/expire_build_artifacts_worker.rb @@ -1,5 +1,6 @@ class ExpireBuildArtifactsWorker include Sidekiq::Worker + include CronjobQueue def perform Rails.logger.info 'Scheduling removal of build artifacts' diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb index 916c2e633c1..eb403c134d1 100644 --- a/app/workers/expire_build_instance_artifacts_worker.rb +++ b/app/workers/expire_build_instance_artifacts_worker.rb @@ -1,11 +1,16 @@ class ExpireBuildInstanceArtifactsWorker include Sidekiq::Worker + include DedicatedSidekiqQueue def perform(build_id) - build = Ci::Build.with_expired_artifacts.reorder(nil).find_by(id: build_id) - return unless build + build = Ci::Build + .with_expired_artifacts + .reorder(nil) + .find_by(id: build_id) - Rails.logger.info "Removing artifacts build #{build.id}..." + return unless build.try(:project) + + Rails.logger.info "Removing artifacts for build #{build.id}..." build.erase_artifacts! end end diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index a6cefd4d601..65f8093b5b0 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -1,8 +1,9 @@ class GitGarbageCollectWorker include Sidekiq::Worker include Gitlab::ShellAdapter + include DedicatedSidekiqQueue - sidekiq_options queue: :gitlab_shell, retry: false + sidekiq_options retry: false def perform(project_id) project = Project.find(project_id) diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb index cfeda88bbc5..964287a1793 100644 --- a/app/workers/gitlab_shell_worker.rb +++ b/app/workers/gitlab_shell_worker.rb @@ -1,8 +1,7 @@ class GitlabShellWorker include Sidekiq::Worker include Gitlab::ShellAdapter - - sidekiq_options queue: :gitlab_shell + include DedicatedSidekiqQueue def perform(action, *arg) gitlab_shell.send(action, *arg) diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb index 5048746f09b..a49a5fd0855 100644 --- a/app/workers/group_destroy_worker.rb +++ b/app/workers/group_destroy_worker.rb @@ -1,7 +1,6 @@ class GroupDestroyWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include DedicatedSidekiqQueue def perform(group_id, user_id) begin diff --git a/app/workers/import_export_project_cleanup_worker.rb b/app/workers/import_export_project_cleanup_worker.rb index 72e3a9ae734..7957ed807ab 100644 --- a/app/workers/import_export_project_cleanup_worker.rb +++ b/app/workers/import_export_project_cleanup_worker.rb @@ -1,7 +1,6 @@ class ImportExportProjectCleanupWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include CronjobQueue def perform ImportExportCleanUpService.new.execute diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index 19f38358eb5..7e44b241743 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -3,6 +3,7 @@ require 'socket' class IrkerWorker include Sidekiq::Worker + include DedicatedSidekiqQueue def perform(project_id, chans, colors, push_data, settings) project = Project.find(project_id) diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb index c87c0a252b1..79efca4f2f9 100644 --- a/app/workers/merge_worker.rb +++ b/app/workers/merge_worker.rb @@ -1,7 +1,6 @@ class MergeWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include DedicatedSidekiqQueue def perform(merge_request_id, current_user_id, params) params = params.with_indifferent_access diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb index 1b3232cd365..c3e62bb88c0 100644 --- a/app/workers/new_note_worker.rb +++ b/app/workers/new_note_worker.rb @@ -1,7 +1,6 @@ class NewNoteWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include DedicatedSidekiqQueue def perform(note_id, note_params) note = Note.find(note_id) diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb index ab5e9f6daad..7e36eacebf8 100644 --- a/app/workers/pipeline_hooks_worker.rb +++ b/app/workers/pipeline_hooks_worker.rb @@ -1,6 +1,6 @@ class PipelineHooksWorker include Sidekiq::Worker - sidekiq_options queue: :default + include PipelineQueue def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id) diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb index 7bb92df3bbd..34f6ef161fb 100644 --- a/app/workers/pipeline_metrics_worker.rb +++ b/app/workers/pipeline_metrics_worker.rb @@ -1,7 +1,6 @@ class PipelineMetricsWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include PipelineQueue def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb index f44227d7086..357e4a9a1c3 100644 --- a/app/workers/pipeline_process_worker.rb +++ b/app/workers/pipeline_process_worker.rb @@ -1,7 +1,6 @@ class PipelineProcessWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include PipelineQueue def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id) diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb index 5dd443fea59..2aa6fff24da 100644 --- a/app/workers/pipeline_success_worker.rb +++ b/app/workers/pipeline_success_worker.rb @@ -1,6 +1,6 @@ class PipelineSuccessWorker include Sidekiq::Worker - sidekiq_options queue: :default + include PipelineQueue def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb index 44a7f24e401..96c4152c674 100644 --- a/app/workers/pipeline_update_worker.rb +++ b/app/workers/pipeline_update_worker.rb @@ -1,7 +1,6 @@ class PipelineUpdateWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include PipelineQueue def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id) diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index a9a2b716005..eee0ca12af9 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -1,7 +1,6 @@ class PostReceive include Sidekiq::Worker - - sidekiq_options queue: :post_receive + include DedicatedSidekiqQueue def perform(repo_path, identifier, changes) if path = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1].to_s) } diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index ccefd0f71a0..71b274e0c99 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -1,9 +1,29 @@ +# Worker for updating any project specific caches. +# +# This worker runs at most once every 15 minutes per project. This is to ensure +# that multiple instances of jobs for this worker don't hammer the underlying +# storage engine as much. class ProjectCacheWorker include Sidekiq::Worker + include DedicatedSidekiqQueue - sidekiq_options queue: :default + LEASE_TIMEOUT = 15.minutes.to_i def perform(project_id) + if try_obtain_lease_for(project_id) + Rails.logger. + info("Obtained ProjectCacheWorker lease for project #{project_id}") + else + Rails.logger. + info("Could not obtain ProjectCacheWorker lease for project #{project_id}") + + return + end + + update_caches(project_id) + end + + def update_caches(project_id) project = Project.find(project_id) return unless project.repository.exists? @@ -15,4 +35,10 @@ class ProjectCacheWorker project.repository.build_cache end end + + def try_obtain_lease_for(project_id) + Gitlab::ExclusiveLease. + new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT). + try_obtain + end end diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb index 3062301a9b1..b462327490e 100644 --- a/app/workers/project_destroy_worker.rb +++ b/app/workers/project_destroy_worker.rb @@ -1,7 +1,6 @@ class ProjectDestroyWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include DedicatedSidekiqQueue def perform(project_id, user_id, params) begin diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb index 615311e63f5..6009aa1b191 100644 --- a/app/workers/project_export_worker.rb +++ b/app/workers/project_export_worker.rb @@ -1,7 +1,8 @@ class ProjectExportWorker include Sidekiq::Worker + include DedicatedSidekiqQueue - sidekiq_options queue: :gitlab_shell, retry: 3 + sidekiq_options retry: 3 def perform(current_user_id, project_id) current_user = User.find(current_user_id) diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb index 64d39c4d3f7..fdfdeab7b41 100644 --- a/app/workers/project_service_worker.rb +++ b/app/workers/project_service_worker.rb @@ -1,7 +1,6 @@ class ProjectServiceWorker include Sidekiq::Worker - - sidekiq_options queue: :project_web_hook + include DedicatedSidekiqQueue def perform(hook_id, data) data = data.with_indifferent_access diff --git a/app/workers/project_web_hook_worker.rb b/app/workers/project_web_hook_worker.rb index fb878965288..efb85eafd15 100644 --- a/app/workers/project_web_hook_worker.rb +++ b/app/workers/project_web_hook_worker.rb @@ -1,7 +1,6 @@ class ProjectWebHookWorker include Sidekiq::Worker - - sidekiq_options queue: :project_web_hook + include DedicatedSidekiqQueue def perform(hook_id, data, hook_name) data = data.with_indifferent_access diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb index 5883cafe1d1..392abb9c21b 100644 --- a/app/workers/prune_old_events_worker.rb +++ b/app/workers/prune_old_events_worker.rb @@ -1,5 +1,6 @@ class PruneOldEventsWorker include Sidekiq::Worker + include CronjobQueue def perform # Contribution calendar shows maximum 12 months of events. diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb index 246c8b6650a..2a619f83410 100644 --- a/app/workers/remove_expired_group_links_worker.rb +++ b/app/workers/remove_expired_group_links_worker.rb @@ -1,5 +1,6 @@ class RemoveExpiredGroupLinksWorker include Sidekiq::Worker + include CronjobQueue def perform ProjectGroupLink.expired.destroy_all diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb index cf765af97ce..31f652e5f9b 100644 --- a/app/workers/remove_expired_members_worker.rb +++ b/app/workers/remove_expired_members_worker.rb @@ -1,5 +1,6 @@ class RemoveExpiredMembersWorker include Sidekiq::Worker + include CronjobQueue def perform Member.expired.find_each do |member| diff --git a/app/workers/repository_archive_cache_worker.rb b/app/workers/repository_archive_cache_worker.rb index a2e49c61f59..e47069df189 100644 --- a/app/workers/repository_archive_cache_worker.rb +++ b/app/workers/repository_archive_cache_worker.rb @@ -1,7 +1,6 @@ class RepositoryArchiveCacheWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include CronjobQueue def perform RepositoryArchiveCleanUpService.new.execute diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb index a3e16fa5212..c3e7491ec4e 100644 --- a/app/workers/repository_check/batch_worker.rb +++ b/app/workers/repository_check/batch_worker.rb @@ -1,14 +1,13 @@ module RepositoryCheck class BatchWorker include Sidekiq::Worker - + include CronjobQueue + RUN_TIME = 3600 - - sidekiq_options retry: false - + def perform start = Time.now - + # This loop will break after a little more than one hour ('a little # more' because `git fsck` may take a few minutes), or if it runs out of # projects to check. By default sidekiq-cron will start a new @@ -17,15 +16,15 @@ module RepositoryCheck project_ids.each do |project_id| break if Time.now - start >= RUN_TIME break unless current_settings.repository_checks_enabled - + next unless try_obtain_lease(project_id) - + SingleRepositoryWorker.new.perform(project_id) end end - + private - + # Project.find_each does not support WHERE clauses and # Project.find_in_batches does not support ordering. So we just build an # array of ID's. This is OK because we do it only once an hour, because @@ -39,7 +38,7 @@ module RepositoryCheck reorder('last_repository_check_at ASC').limit(limit).pluck(:id) never_checked_projects + old_check_projects end - + def try_obtain_lease(id) # Use a 24-hour timeout because on servers/projects where 'git fsck' is # super slow we definitely do not want to run it twice in parallel. @@ -48,7 +47,7 @@ module RepositoryCheck timeout: 24.hours ).try_obtain end - + def current_settings # No caching of the settings! If we cache them and an admin disables # this feature, an active RepositoryCheckWorker would keep going for up diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb index b7202ddff34..1f1b38540ee 100644 --- a/app/workers/repository_check/clear_worker.rb +++ b/app/workers/repository_check/clear_worker.rb @@ -1,8 +1,7 @@ module RepositoryCheck class ClearWorker include Sidekiq::Worker - - sidekiq_options retry: false + include RepositoryCheckQueue def perform # Do small batched updates because these updates will be slow and locking diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb index 98ddf5d0688..3d8bfc6fc6c 100644 --- a/app/workers/repository_check/single_repository_worker.rb +++ b/app/workers/repository_check/single_repository_worker.rb @@ -1,8 +1,7 @@ module RepositoryCheck class SingleRepositoryWorker include Sidekiq::Worker - - sidekiq_options retry: false + include RepositoryCheckQueue def perform(project_id) project = Project.find(project_id) diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index 61ed1c38ac4..efc99ec962a 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -1,8 +1,7 @@ class RepositoryForkWorker include Sidekiq::Worker include Gitlab::ShellAdapter - - sidekiq_options queue: :gitlab_shell + include DedicatedSidekiqQueue def perform(project_id, forked_from_repository_storage_path, source_path, target_path) Gitlab::Metrics.add_event(:fork_repository, diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index d2ca8813ab9..c8a77e21c12 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -1,8 +1,7 @@ class RepositoryImportWorker include Sidekiq::Worker include Gitlab::ShellAdapter - - sidekiq_options queue: :gitlab_shell + include DedicatedSidekiqQueue attr_accessor :project, :current_user diff --git a/app/workers/requests_profiles_worker.rb b/app/workers/requests_profiles_worker.rb index 9dd228a2483..703b025d76e 100644 --- a/app/workers/requests_profiles_worker.rb +++ b/app/workers/requests_profiles_worker.rb @@ -1,7 +1,6 @@ class RequestsProfilesWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include CronjobQueue def perform Gitlab::RequestProfiler.remove_all_profiles diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb index 6828013b377..b70df5a1afa 100644 --- a/app/workers/stuck_ci_builds_worker.rb +++ b/app/workers/stuck_ci_builds_worker.rb @@ -1,5 +1,6 @@ class StuckCiBuildsWorker include Sidekiq::Worker + include CronjobQueue BUILD_STUCK_TIMEOUT = 1.day diff --git a/app/workers/system_hook_worker.rb b/app/workers/system_hook_worker.rb index a122c274763..baf2f12eeac 100644 --- a/app/workers/system_hook_worker.rb +++ b/app/workers/system_hook_worker.rb @@ -1,7 +1,6 @@ class SystemHookWorker include Sidekiq::Worker - - sidekiq_options queue: :system_hook + include DedicatedSidekiqQueue def perform(hook_id, data, hook_name) SystemHook.find(hook_id).execute(data, hook_name) diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb index df4c4a6628b..0531630d13a 100644 --- a/app/workers/trending_projects_worker.rb +++ b/app/workers/trending_projects_worker.rb @@ -1,7 +1,6 @@ class TrendingProjectsWorker include Sidekiq::Worker - - sidekiq_options queue: :trending_projects + include CronjobQueue def perform Rails.logger.info('Refreshing trending projects') diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb index 03f0528cdae..acc4d858136 100644 --- a/app/workers/update_merge_requests_worker.rb +++ b/app/workers/update_merge_requests_worker.rb @@ -1,5 +1,6 @@ class UpdateMergeRequestsWorker include Sidekiq::Worker + include DedicatedSidekiqQueue def perform(project_id, user_id, oldrev, newrev, ref) project = Project.find_by(id: project_id) |