diff options
author | Lin Jen-Shin <godfat@godfat.org> | 2016-10-14 14:31:10 +0300 |
---|---|---|
committer | Lin Jen-Shin <godfat@godfat.org> | 2016-10-14 14:31:10 +0300 |
commit | dc1d269f67f63eab5f358306ce394b5831377bf7 (patch) | |
tree | 2fb75d51a54d6b19e783c11cf12942d7a67cf8db /app | |
parent | b5f9d4c4bc48b252d3175432a3bb6fb1ca394af9 (diff) | |
parent | ca3bef554b14ddd2a0d844cd64874885e3f4e90e (diff) |
Merge remote-tracking branch 'upstream/master' into pipeline-emails
* upstream/master: (237 commits)
Grapify boards API
Add test, fix merge error
Use local assigns to get the dropdown title
Updated issuable dropdown titles
Added safety check for formatted values
Minor style improvement
Fixed conflict and corrected teaspoon test
Rename method in test
Moved ci_status environments logic to new action ci_envrionments_status and set up frontend polling
Refactor ci_status on MergeRequestController
Fix indenting error in HAML
Show what time ago a MR was deployed
Fixed missing links
Fixed missing links
Refactor merge requests revisions
Add link to update docs for source installations
Grapify todos API
Link to review apps example from docs
fix grafana_configuration.md move link
Do not run before_script, artifacts, cache in trigger_docs job
...
Diffstat (limited to 'app')
136 files changed, 1062 insertions, 646 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 599331df3f5..56ec1489f89 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -6,11 +6,10 @@ groupProjectsPath: "/api/:version/groups/:id/projects.json", projectsPath: "/api/:version/projects.json?simple=true", labelsPath: "/:namespace_path/:project_path/labels", - licensePath: "/api/:version/licenses/:key", - gitignorePath: "/api/:version/gitignores/:key", - gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key", + licensePath: "/api/:version/templates/licenses/:key", + gitignorePath: "/api/:version/templates/gitignores/:key", + gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key", issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key", - group: function(group_id, callback) { var url = Api.buildUrl(Api.groupPath) .replace(':id', group_id); diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 91c12570e09..d4f8f4b9420 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -28,12 +28,13 @@ $(() => { state: Store.state, loading: true, endpoint: $boardApp.dataset.endpoint, + boardId: $boardApp.dataset.boardId, disabled: $boardApp.dataset.disabled === 'true', issueLinkBase: $boardApp.dataset.issueLinkBase }, init: Store.create.bind(Store), created () { - gl.boardService = new BoardService(this.endpoint); + gl.boardService = new BoardService(this.endpoint, this.boardId); }, ready () { Store.disabled = this.disabled; diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index 2b825c3949f..b9c91cbf31e 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -1,15 +1,15 @@ class BoardService { - constructor (root) { + constructor (root, boardId) { Vue.http.options.root = root; - this.lists = Vue.resource(`${root}/lists{/id}`, {}, { + this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { generate: { method: 'POST', - url: `${root}/lists/generate.json` + url: `${root}/${boardId}/lists/generate.json` } }); - this.issue = Vue.resource(`${root}/issues{/id}`, {}); - this.issues = Vue.resource(`${root}/lists{/id}/issues`, {}); + this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {}); + this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}); Vue.http.interceptors.push((request, next) => { request.headers['X-CSRF-Token'] = $.rails.csrfToken(); diff --git a/app/assets/javascripts/commit/image-file.js b/app/assets/javascripts/commit/image_file.js index e893491b19b..e893491b19b 100644 --- a/app/assets/javascripts/commit/image-file.js +++ b/app/assets/javascripts/commit/image_file.js diff --git a/app/assets/javascripts/cycle-analytics.js.es6 b/app/assets/javascripts/cycle_analytics.js.es6 index cd9886ba58d..cd9886ba58d 100644 --- a/app/assets/javascripts/cycle-analytics.js.es6 +++ b/app/assets/javascripts/cycle_analytics.js.es6 diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 8d99b12102d..f3ef13ce20e 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -21,6 +21,7 @@ shortcut_handler = null; switch (page) { case 'projects:boards:show': + case 'projects:boards:index': shortcut_handler = new ShortcutsNavigation(); break; case 'projects:merge_requests:index': @@ -126,6 +127,9 @@ new TreeView(); } break; + case 'projects:pipelines:show': + new gl.Pipelines(); + break; case 'groups:activity': new Activities(); break; diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index d0786bf0053..845313b6b38 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -52,37 +52,27 @@ } } }, - setup: function(input) { + setup: _.debounce(function(input) { // Add GFM auto-completion to all input fields, that accept GFM input. this.input = input || $('.js-gfm-input'); // destroy previous instances this.destroyAtWho(); // set up instances this.setupAtWho(); - if (this.dataSource) { - if (!this.dataLoading && !this.cachedData) { - this.dataLoading = true; - setTimeout((function(_this) { - return function() { - var fetch; - fetch = _this.fetchData(_this.dataSource); - return fetch.done(function(data) { - _this.dataLoading = false; - return _this.loadData(data); - }); - }; - // We should wait until initializations are done - // and only trigger the last .setup since - // The previous .dataSource belongs to the previous issuable - // and the last one will have the **proper** .dataSource property - // TODO: Make this a singleton and turn off events when moving to another page - })(this), 1000); - } - if (this.cachedData != null) { - return this.loadData(this.cachedData); - } + + if (this.dataSource && !this.dataLoading && !this.cachedData) { + this.dataLoading = true; + return this.fetchData(this.dataSource) + .done((data) => { + this.dataLoading = false; + this.loadData(data); + }); + }; + + if (this.cachedData != null) { + return this.loadData(this.cachedData); } - }, + }, 1000), setupAtWho: function() { // Emoji this.input.atwho({ diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index d4403375643..e034ca68645 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -738,6 +738,7 @@ return false; } if (currentKeyCode === 13 && currentIndex !== -1) { + e.preventDefault(); _this.selectRowAtIndex(); } }; diff --git a/app/assets/javascripts/issues-bulk-assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6 index 0808f538f01..0808f538f01 100644 --- a/app/assets/javascripts/issues-bulk-assignment.js.es6 +++ b/app/assets/javascripts/issues_bulk_assignment.js.es6 diff --git a/app/assets/javascripts/LabelManager.js.es6 b/app/assets/javascripts/label_manager.js.es6 index bc68e53504f..bc68e53504f 100644 --- a/app/assets/javascripts/LabelManager.js.es6 +++ b/app/assets/javascripts/label_manager.js.es6 diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index e356872624a..f1e719937c7 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -292,7 +292,7 @@ return; } - if (page === 'projects:boards:show') { + if ($('html').hasClass('issue-boards-page')) { return; } if ($dropdown.hasClass('js-multiselect')) { @@ -334,7 +334,7 @@ page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = page === 'projects:merge_requests:index'; - if (page === 'projects:boards:show') { + if ($('html').hasClass('issue-boards-page')) { if (label.isAny) { gl.issueBoards.BoardsStore.state.filters['label_name'] = []; } diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js.es6 index 7bbcdf59838..fcadc4bc515 100644 --- a/app/assets/javascripts/merge_request_widget.js +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -1,7 +1,26 @@ -(function() { + ((global) => { var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; - this.MergeRequestWidget = (function() { + const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>"> + <div class="ci_widget ci-success"> + <%= ci_success_icon %> + <span> + Deployed to + <a href="<%- url %>" target="_blank" class="environment"> + <%- name %> + </a> + <span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>"> + <%- deployed_at %> + </span> + <a class="js-environment-link" href="<%- external_url %>" target="_blank"> + <i class="fa fa-external-link"></i> + View on <%- external_url_formatted %> + </a> + </span> + </div> + </div>`; + + global.MergeRequestWidget = (function() { function MergeRequestWidget(opts) { // Initialize MergeRequestWidget behavior // @@ -10,17 +29,23 @@ // ci_status_url - String, URL to use to check CI status // this.opts = opts; + this.$widgetBody = $('.mr-widget-body'); $('#modal_merge_info').modal({ show: false }); this.firstCICheck = true; this.readyForCICheck = false; + this.readyForCIEnvironmentCheck = false; this.cancel = false; clearInterval(this.fetchBuildStatusInterval); + clearInterval(this.fetchBuildEnvironmentStatusInterval); this.clearEventListeners(); this.addEventListeners(); this.getCIStatus(false); + this.getCIEnvironmentsStatus(); + this.retrieveSuccessIcon(); this.pollCIStatus(); + this.pollCIEnvironmentsStatus(); notifyPermissions(); } @@ -41,6 +66,7 @@ page = $('body').data('page').split(':').last(); if (allowedPages.indexOf(page) < 0) { clearInterval(_this.fetchBuildStatusInterval); + clearInterval(_this.fetchBuildEnvironmentStatusInterval); _this.cancelPolling(); return _this.clearEventListeners(); } @@ -48,6 +74,12 @@ })(this)); }; + MergeRequestWidget.prototype.retrieveSuccessIcon = function() { + const $ciSuccessIcon = $('.js-success-icon'); + this.$ciSuccessIcon = $ciSuccessIcon.html(); + $ciSuccessIcon.remove(); + } + MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) { if (deleteSourceBranch == null) { deleteSourceBranch = false; @@ -62,7 +94,7 @@ urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : ''; return window.location.href = window.location.pathname + urlSuffix; } else if (data.merge_error) { - return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>"); + return this.$widgetBody.html("<h4>" + data.merge_error + "</h4>"); } else { callback = function() { return merge_request_widget.mergeInProgress(deleteSourceBranch); @@ -118,6 +150,7 @@ if (data.status === '') { return; } + if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) { _this.opts.ci_status = data.status; _this.showCIStatus(data.status); @@ -150,6 +183,41 @@ })(this)); }; + MergeRequestWidget.prototype.pollCIEnvironmentsStatus = function() { + this.fetchBuildEnvironmentStatusInterval = setInterval(() => { + if (!this.readyForCIEnvironmentCheck) return; + this.getCIEnvironmentsStatus(); + this.readyForCIEnvironmentCheck = false; + }, 300000); + }; + + MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() { + $.getJSON(this.opts.ci_environments_status_url, (environments) => { + if (this.cancel) return; + this.readyForCIEnvironmentCheck = true; + if (environments && environments.length) this.renderEnvironments(environments); + }); + }; + + MergeRequestWidget.prototype.renderEnvironments = function(environments) { + for (let i = 0; i < environments.length; i++) { + const environment = environments[i]; + 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.deployed_at && environment.deployed_at_formatted) { + environment.deployed_at = $.timeago(environment.deployed_at) + '.'; + } else { + $('.js-environment-timeago', $template).remove(); + environment.name += '.'; + } + environment.ci_success_icon = this.$ciSuccessIcon; + const templateString = _.unescape($template[0].outerHTML); + const template = _.template(templateString)(environment) + this.$widgetBody.before(template); + } + }; + MergeRequestWidget.prototype.showCIStatus = function(state) { var allowed_states; if (state == null) { @@ -190,4 +258,4 @@ })(); -}).call(this); + })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 26cc6eb0e96..cee42633c79 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -110,7 +110,7 @@ e.preventDefault(); return; } - if (page === 'projects:boards:show') { + if ($('html').hasClass('issue-boards-page')) { gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name; gl.issueBoards.BoardsStore.updateFiltersUrl(); e.preventDefault(); diff --git a/app/assets/javascripts/network/branch-graph.js b/app/assets/javascripts/network/branch_graph.js index 91132af273a..91132af273a 100644 --- a/app/assets/javascripts/network/branch-graph.js +++ b/app/assets/javascripts/network/branch_graph.js diff --git a/app/assets/javascripts/pipeline.js.es6 b/app/assets/javascripts/pipeline.js.es6 index 8813bb5dfef..6bf63ee6979 100644 --- a/app/assets/javascripts/pipeline.js.es6 +++ b/app/assets/javascripts/pipeline.js.es6 @@ -1,24 +1,40 @@ -(function() { - function toggleGraph() { - const $pipelineBtn = $(this).closest('.toggle-pipeline-btn'); - const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph'); - const $btnText = $(this).find('.toggle-btn-text'); - const $icon = $(this).find('.fa'); - - $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed'); - - const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed'); - const expandIcon = 'fa-caret-down'; - const hideIcon = 'fa-caret-up'; - - if(graphCollapsed) { - $btnText.text('Expand'); - $icon.removeClass(hideIcon).addClass(expandIcon); - } else { - $btnText.text('Hide'); - $icon.removeClass(expandIcon).addClass(hideIcon); +((global) => { + + class Pipelines { + constructor() { + $(document).off('click', '.toggle-pipeline-btn').on('click', '.toggle-pipeline-btn', this.toggleGraph); + this.addMarginToBuildColumns(); + } + + toggleGraph() { + const $pipelineBtn = $(this).closest('.toggle-pipeline-btn'); + const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph'); + const $btnText = $(this).find('.toggle-btn-text'); + const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed'); + + $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed'); + + + graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide') + } + + addMarginToBuildColumns() { + const $secondChildBuildNode = $('.build:nth-child(2)'); + if ($secondChildBuildNode.length) { + const $firstChildBuildNode = $secondChildBuildNode.prev('.build'); + const $multiBuildColumn = $secondChildBuildNode.closest('.stage-column'); + const $previousColumn = $multiBuildColumn.prev('.stage-column'); + $multiBuildColumn.addClass('left-margin'); + $firstChildBuildNode.addClass('left-connector'); + $previousColumn.each(function() { + $this = $(this); + if ($('.build', $this).length === 1) $this.addClass('no-margin'); + }); + } + $('.pipeline-graph').removeClass('hidden'); } } - $(document).on('click', '.toggle-pipeline-btn', toggleGraph); -})(); + global.Pipelines = Pipelines; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js index a787b11f2a9..3cf41505814 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/project_new.js @@ -4,7 +4,9 @@ this.ProjectNew = (function() { function ProjectNew() { this.toggleSettings = bind(this.toggleSettings, this); - this.$selects = $('.features select'); + this.$selects = $('.features select').filter(function () { + return $(this).data('field'); + }); $('.project-edit-container').on('ajax:before', (function(_this) { return function() { diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index bcabda3ceb2..6aa0e1cd2b6 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -160,7 +160,7 @@ selectedId = user.id; return; } - if (page === 'projects:boards:show') { + if ($('html').hasClass('issue-boards-page')) { selectedId = user.id; gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id; gl.issueBoards.BoardsStore.updateFiltersUrl(); @@ -261,10 +261,11 @@ } } if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) { + var trimmed = query.term.trim(); emailUser = { name: "Invite \"" + query.term + "\"", - username: query.term, - id: query.term + username: trimmed, + id: trimmed }; data.results.unshift(emailUser); } diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index c79b22d4d21..98e301d3799 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -4,7 +4,7 @@ width: 40px; height: 40px; padding: 0; - @include border-radius($avatar_radius); + border-radius: $avatar_radius; border: 1px solid rgba(0, 0, 0, .1); &.avatar-inline { @@ -17,7 +17,7 @@ } &.avatar-tile { - @include border-radius(0); + border-radius: 0; border: none; } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index d315db4cb32..8002e56724b 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -133,7 +133,7 @@ } .identicon { - @include border-radius(50%); + border-radius: 50%; } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index d11b2fe7ec2..a7c8d782e9b 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -1,5 +1,5 @@ @mixin btn-default { - @include border-radius(3px); + border-radius: 3px; font-size: $gl-font-size; font-weight: 500; padding: $gl-vert-padding $gl-btn-padding; @@ -8,7 +8,7 @@ &:active { outline: none; background-color: $btn-active-gray; - @include box-shadow($gl-btn-active-background); + box-shadow: $gl-btn-active-background; } } @@ -43,7 +43,7 @@ &:active, &.active { - @include box-shadow ($gl-btn-active-background); + box-shadow: $gl-btn-active-background; background-color: $dark; border-color: $border-dark; @@ -279,7 +279,7 @@ } .active { - @include box-shadow($gl-btn-active-background); + box-shadow: $gl-btn-active-background; border: 1px solid #c6cacf !important; background-color: #e4e7ed !important; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index a67d31de2f7..05e8ee0190d 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -73,7 +73,7 @@ label { } .form-control { - @include box-shadow(none); + box-shadow: none; border-radius: 3px; padding: $gl-vert-padding $gl-input-padding; } diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 8bfc0d583c5..ba3930e03bd 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -16,7 +16,7 @@ margin-top: 5px; } - @include border-radius(3px); + border-radius: 3px; display: block; float: left; margin-right: 10px; diff --git a/app/assets/stylesheets/framework/logo.scss b/app/assets/stylesheets/framework/logo.scss index 3ee3fb4cee5..c214eabcad7 100644 --- a/app/assets/stylesheets/framework/logo.scss +++ b/app/assets/stylesheets/framework/logo.scss @@ -1,15 +1,3 @@ -@mixin unique-keyframes { - $animation-name: unique-id(); - @include webkit-prefix(animation-name, $animation-name); - - @-webkit-keyframes #{$animation-name} { - @content; - } - @keyframes #{$animation-name} { - @content; - } -} - @mixin tanuki-logo-colors($path-color) { fill: $path-color; transition: all 0.8s; @@ -20,28 +8,6 @@ } } -@mixin tanuki-second-highlight-animations($tanuki-color) { - @include unique-keyframes { - 10%, 80% { - fill: #{$tanuki-color} - } - 20%, 90% { - fill: lighten($tanuki-color, 25%); - } - } -} - -@mixin tanuki-forth-highlight-animations($tanuki-color) { - @include unique-keyframes { - 30%, 60% { - fill: #{$tanuki-color}; - } - 40%, 70% { - fill: lighten($tanuki-color, 25%); - } - } -} - .tanuki-logo { .tanuki-left-ear, @@ -67,7 +33,7 @@ } .tanuki-left-cheek { - @include unique-keyframes { + @include include-keyframes(animate-tanuki-left-cheek) { 0%, 10%, 100% { fill: lighten($tanuki-yellow, 25%); } @@ -78,15 +44,29 @@ } .tanuki-left-eye { - @include tanuki-second-highlight-animations($tanuki-orange); + @include include-keyframes(animate-tanuki-left-eye) { + 10%, 80% { + fill: $tanuki-orange; + } + 20%, 90% { + fill: lighten($tanuki-orange, 25%); + } + } } .tanuki-left-ear { - @include tanuki-second-highlight-animations($tanuki-red); + @include include-keyframes(animate-tanuki-left-ear) { + 10%, 80% { + fill: $tanuki-red; + } + 20%, 90% { + fill: lighten($tanuki-red, 25%); + } + } } .tanuki-nose { - @include unique-keyframes { + @include include-keyframes(animate-tanuki-nose) { 20%, 70% { fill: $tanuki-red; } @@ -97,15 +77,29 @@ } .tanuki-right-eye { - @include tanuki-forth-highlight-animations($tanuki-orange); + @include include-keyframes(animate-tanuki-right-eye) { + 30%, 60% { + fill: $tanuki-orange; + } + 40%, 70% { + fill: lighten($tanuki-orange, 25%); + } + } } .tanuki-right-ear { - @include tanuki-forth-highlight-animations($tanuki-red); + @include include-keyframes(animate-tanuki-right-ear) { + 30%, 60% { + fill: $tanuki-red; + } + 40%, 70% { + fill: lighten($tanuki-red, 25%); + } + } } .tanuki-right-cheek { - @include unique-keyframes { + @include include-keyframes(animate-tanuki-right-cheek) { 40% { fill: $tanuki-yellow; } @@ -115,4 +109,4 @@ } } } -}
\ No newline at end of file +} diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index edea4ad00eb..6d28d98b283 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -86,7 +86,7 @@ } .markdown-area { - @include border-radius(0); + border-radius: 0; background: #fff; border: 1px solid #ddd; min-height: 140px; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 1ec08cdef23..7fabf27a558 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -1,15 +1,4 @@ /** - * Generic mixins - */ -@mixin box-shadow($shadow) { - box-shadow: $shadow; -} - -@mixin border-radius($radius) { - border-radius: $radius; -} - -/** * Prefilled mixins * Mixins with fixed values */ @@ -95,3 +84,10 @@ @content; } } + +@mixin include-keyframes($animation-name) { + @include webkit-prefix(animation-name, $animation-name); + @include keyframes($animation-name) { + @content; + } +} diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 76b93b23b95..9fe390eb09d 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -133,5 +133,5 @@ font-size: 20px; color: #777; z-index: 100; - @include box-shadow(0 1px 2px #ddd); + box-shadow: 0 1px 2px #ddd; } diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index bcd60391543..79cd26714a3 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -46,8 +46,8 @@ } .select2-drop { - @include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0); - @include border-radius ($border-radius-default); + box-shadow: rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0; + border-radius: $border-radius-default; border: none; min-width: 175px; } @@ -72,7 +72,7 @@ .select2-container-active { .select2-choice, .select2-choices { - @include box-shadow(none); + box-shadow: none; } } @@ -82,13 +82,13 @@ outline: 0; background-image: none; background-color: $white-dark; - @include box-shadow($gl-btn-active-gradient); + box-shadow: $gl-btn-active-gradient; } } .select2-container-multi { .select2-choices { - @include border-radius($border-radius-default); + border-radius: $border-radius-default; border-color: $input-border; background: none; @@ -123,7 +123,7 @@ &.select2-container-active .select2-choices, &.select2-dropdown-open .select2-choices { border-color: $border-white-normal; - @include box-shadow($gl-btn-active-gradient); + box-shadow: $gl-btn-active-gradient; } } @@ -157,7 +157,7 @@ background-repeat: no-repeat; background-position: right 0 bottom 6px; border: 1px solid $input-border; - @include border-radius($border-radius-default); + border-radius: $border-radius-default; transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; &:focus { diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 557ef7291cf..ec52f326eb9 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -4,7 +4,7 @@ &.page-sidebar-pinned { .sidebar-wrapper { - @include box-shadow(none); + box-shadow: none; } } @@ -17,7 +17,7 @@ width: 0; overflow: hidden; transition: width $sidebar-transition-duration; - @include box-shadow(2px 0 16px 0 $black-transparent); + box-shadow: 2px 0 16px 0 $black-transparent; } } @@ -100,7 +100,7 @@ .count { float: right; padding: 0 8px; - @include border-radius(6px); + border-radius: 6px; } } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 9f2d53d5206..d099a884f54 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -116,7 +116,7 @@ font-size: 13px; line-height: 1.6em; overflow-x: auto; - @include border-radius(2px); + border-radius: 2px; } p > code { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 14ec310de2d..4c34ed3ebf7 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -17,8 +17,10 @@ $white-normal: #ededed; $white-dark: #ececec; $gray-light: #fafafa; +$gray-lighter: #f9f9f9; $gray-normal: #f5f5f5; $gray-dark: #ededed; +$gray-darker: #eee; $gray-darkest: #c9c9c9; $green-light: #38ae67; @@ -33,6 +35,8 @@ $blue-medium-light: #3498cb; $blue-medium: #2f8ebf; $blue-medium-dark: #2d86b4; +$blue-light-transparent: rgba(44, 159, 216, 0.05); + $orange-light: #fc8a51; $orange-normal: #e75e40; $orange-dark: #ce5237; @@ -91,6 +95,7 @@ $table-text-gray: #8f8f8f; $gl-font-size: 15px; $gl-title-color: #333; $gl-text-color: #5c5c5c; +$gl-text-color-light: #8c8c8c; $gl-text-green: #4a2; $gl-text-red: #d12f19; $gl-text-orange: #d90; diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 778471a34d7..d732008de3d 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -50,7 +50,7 @@ .bordered-box { border: 1px solid $border-color; - @include border-radius($border-radius-default); + border-radius: $border-radius-default; } diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index e1304335271..fcc5f32c738 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -1,7 +1,7 @@ .file-editor { #editor { border: none; - @include border-radius(0); + border-radius: 0; height: 500px; margin: 0; padding: 0; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 1d00da1266c..789d6237df8 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -91,7 +91,7 @@ float: right; border: 1px solid #eee; padding: 5px; - @include border-radius(5px); + border-radius: 5px; background: $gray-light; margin-left: 10px; top: -6px; diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 822830706a5..701c29a3986 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -1,7 +1,7 @@ .suggest-colors { margin-top: 5px; a { - @include border-radius(4px); + border-radius: 4px; width: 30px; height: 30px; display: inline-block; @@ -17,7 +17,7 @@ overflow: hidden; a { - @include border-radius(0); + border-radius: 0; width: (100% / 7); margin-right: 0; margin-bottom: -5px; diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 403171d4532..a5ca509163d 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -73,12 +73,12 @@ height: auto; &.top { - @include border-radius(5px 5px 0 0); + border-radius: 5px 5px 0 0; margin-bottom: 0; } &.bottom { - @include border-radius(0 0 5px 5px); + border-radius: 0 0 5px 5px; border-top: 0; margin-bottom: 20px; } @@ -86,7 +86,7 @@ &.middle { border-top: 0; margin-bottom: 0; - @include border-radius(0); + border-radius: 0; } &:active, &:focus { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index bc8693ae467..6a0fae8a3f9 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -6,7 +6,7 @@ background: $background-color; color: $gl-gray; border: 1px solid $border-color; - @include border-radius(2px); + border-radius: 2px; form { margin-bottom: 0; @@ -121,6 +121,10 @@ color: #5c5d5e; } + .js-deployment-link { + display: inline-block; + } + .mr-widget-body { h4 { font-weight: 600; @@ -204,6 +208,18 @@ word-break: break-all; } +.commits-empty { + text-align: center; + + h4 { + padding-top: 20px; + padding-bottom: 10px; + } + svg { + width: 230px; + } +} + .mr-list { .merge-request { padding: 10px 15px; @@ -389,8 +405,12 @@ padding: 16px; } + .content-block { + border-top: 1px solid $border-color; + padding: $gl-padding-top $gl-padding; + } + .comments-disabled-notif { - padding: 10px 16px; .btn { margin-left: 5px; } @@ -401,10 +421,6 @@ margin: 0 7px; } - .comments-disabled-notif { - border-top: 1px solid $border-color; - } - .dropdown-title { color: $gl-text-color; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 54124a3d658..d399f84a2ff 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -334,7 +334,7 @@ ul.notes { .add-diff-note { margin-top: -4px; - @include border-radius(40px); + border-radius: 40px; background: #fff; padding: 4px; font-size: 16px; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index a2779704eff..7843355f0ab 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -303,16 +303,41 @@ .stage-column { display: inline-block; vertical-align: top; - margin-right: 65px; + + &:not(:last-child) { + margin-right: 44px; + } + + &.left-margin { + &:not(:first-child) { + margin-left: 44px; + + .left-connector { + &::before { + content: ''; + position: absolute; + top: 48%; + left: -48px; + border-top: 2px solid $border-color; + width: 48px; + height: 1px; + } + } + } + } + + &.no-margin { + margin: 0; + } li { list-style: none; } .stage-name { - margin-bottom: 15px; + margin: 0 0 15px 10px; font-weight: bold; - width: 150px; + width: 176px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -321,17 +346,23 @@ .build { border: 1px solid $border-color; position: relative; - padding: 6px 10px; + padding: 7px 10px 8px; border-radius: 30px; - width: 150px; + width: 186px; margin-bottom: 10px; + &:hover { + background-color: $gray-lighter; + .dropdown-menu-toggle { + background-color: transparent; + } + } + &.playable { - background-color: $gray-light; svg { - height: 12px; - width: 12px; + height: 13px; + width: 20px; position: relative; top: 1px; @@ -342,10 +373,20 @@ } .build-content { - width: 130px; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + width: 164px; + + .ci-status-icon { + svg { + height: 20px; + width: 20px; + } + } .ci-status-text { - width: 110px; + width: 135px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -356,44 +397,56 @@ } a { - color: $layout-link-gray; + color: $gl-text-color-light; text-decoration: none; - - &:hover { - .ci-status-text { - text-decoration: underline; - } - } } .dropdown-menu-toggle { border: none; width: auto; padding: 0; - color: $layout-link-gray; + color: $gl-text-color-light; + flex-grow: 1; .ci-status-text { - width: 80px; + max-width: 112px; + width: auto; } } .grouped-pipeline-dropdown { padding: 8px 0; - width: 200px; + width: 186px; left: auto; - right: -214px; + right: -197px; top: -9px; - max-height: 245px; - overflow-y: scroll; - a:hover { - .ci-status-text { - text-decoration: none; + ul { + max-height: 245px; + overflow: auto; + } + + a { + color: $gl-text-color; + padding: 7px 8px 8px; + + &:hover { + background-color: $blue-light-transparent; + border-radius: 3px; + + .ci-status-text { + text-decoration: none; + } } } + svg { + width: 14px; + height: 14px; + } + .ci-status-text { - width: 145px; + width: 112px; } .arrow { @@ -426,9 +479,10 @@ } .badge { - background-color: $gray-dark; - color: $layout-link-gray; + background-color: $gray-darker; + color: $gl-text-color-light; font-weight: normal; + margin-left: $btn-xs-side-margin; } } @@ -442,10 +496,10 @@ &::after { content: ''; position: absolute; - top: 50%; - right: -69px; + top: 48%; + right: -48px; border-top: 2px solid $border-color; - width: 69px; + width: 48px; height: 1px; } } @@ -454,25 +508,25 @@ &:not(:first-child) { &::after, &::before { content: ''; - top: -47px; + top: -49px; position: absolute; border-bottom: 2px solid $border-color; - width: 20px; - height: 65px; + width: 25px; + height: 69px; } // Right connecting curves &::after { - right: -20px; + right: -25px; border-right: 2px solid $border-color; - border-radius: 0 0 15px; + border-radius: 0 0 20px; } // Left connecting curves &::before { - left: -20px; + left: -25px; border-left: 2px solid $border-color; - border-radius: 0 0 0 15px; + border-radius: 0 0 0 20px; } } @@ -480,7 +534,7 @@ &:nth-child(2) { &::after, &::before { height: 29px; - top: -10px; + top: -9px; } .curve { display: block; @@ -538,20 +592,20 @@ width: 21px; height: 25px; position: absolute; - top: -29px; + top: -32px; border-top: 2px solid $border-color; } &::after { - left: -39px; + left: -44px; border-right: 2px solid $border-color; - border-radius: 0 15px; + border-radius: 0 20px; } &::before { - right: -39px; + right: -44px; border-left: 2px solid $border-color; - border-radius: 15px 0 0; + border-radius: 20px 0 0; } } } diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss index e5859fe7384..f8da0983b77 100644 --- a/app/assets/stylesheets/pages/profiles/preferences.scss +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -4,7 +4,7 @@ text-align: center; .preview { - @include border-radius(4px); + border-radius: 4px; height: 80px; margin-bottom: 10px; @@ -47,7 +47,7 @@ width: 160px; img { - @include border-radius(4px); + border-radius: 4px; max-width: 100%; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 87548dcb590..530fb0c0d05 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -354,7 +354,7 @@ a.deploy-project-label { justify-content: flex-start; .fork-thumbnail { - @include border-radius($border-radius-base); + border-radius: $border-radius-base; background-color: $white-light; border: 1px solid $border-white-light; height: 202px; @@ -371,7 +371,7 @@ a.deploy-project-label { background-color: $gray-light; border: 1px solid $gray-dark; margin: 0 auto; - @include border-radius(50%); + border-radius: 50%; i { font-size: 100px; color: $gray-dark; @@ -390,7 +390,7 @@ a.deploy-project-label { } img { - @include border-radius(50%); + border-radius: 50%; max-width: 100px; } } @@ -496,7 +496,7 @@ pre.light-well { } .light-well { - @include border-radius (2px); + border-radius: 2px; color: #5b6169; font-size: 13px; diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 0ee7ceecae5..c05f3d5ff32 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -4,7 +4,7 @@ margin-right: 10px; border: 1px solid #eee; white-space: nowrap; - @include border-radius(4px); + border-radius: 4px; &:hover { text-decoration: none; diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb deleted file mode 100644 index 5bb7d499cdc..00000000000 --- a/app/controllers/ci/application_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Ci - class ApplicationController < ::ApplicationController - def self.railtie_helpers_paths - "app/helpers/ci" - end - end -end diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb index 78012960252..3eb485de9db 100644 --- a/app/controllers/ci/lints_controller.rb +++ b/app/controllers/ci/lints_controller.rb @@ -1,5 +1,5 @@ module Ci - class LintsController < ApplicationController + class LintsController < ::ApplicationController before_action :authenticate_user! def show diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index aa894fde36b..ff297d6ff13 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -1,5 +1,5 @@ module Ci - class ProjectsController < Ci::ApplicationController + class ProjectsController < ::ApplicationController before_action :project before_action :no_cache, only: [:badge] before_action :authorize_read_project!, except: [:badge, :index] diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 38e5943eb76..a62c6211372 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -21,8 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController end def trending - @projects = TrendingProjectsFinder.new.execute - @projects = filter_projects(@projects) + @projects = filter_projects(Project.trending) @projects = @projects.page(params[:page]) respond_to do |format| diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb deleted file mode 100644 index 83eec1bf4a2..00000000000 --- a/app/controllers/namespaces_controller.rb +++ /dev/null @@ -1,25 +0,0 @@ -class NamespacesController < ApplicationController - skip_before_action :authenticate_user! - - def show - namespace = Namespace.find_by(path: params[:id]) - - if namespace - if namespace.is_a?(Group) - group = namespace - else - user = namespace.owner - end - end - - if user - redirect_to user_path(user) - elsif group && can?(current_user, :read_group, group) - redirect_to group_path(group) - elsif current_user.nil? - authenticate_user! - else - render_404 - end - end -end diff --git a/app/controllers/projects/board_lists_controller.rb b/app/controllers/projects/board_lists_controller.rb deleted file mode 100644 index 3cfb08d5822..00000000000 --- a/app/controllers/projects/board_lists_controller.rb +++ /dev/null @@ -1,65 +0,0 @@ -class Projects::BoardListsController < Projects::ApplicationController - respond_to :json - - before_action :authorize_admin_list! - - rescue_from ActiveRecord::RecordNotFound, with: :record_not_found - - def create - list = Boards::Lists::CreateService.new(project, current_user, list_params).execute - - if list.valid? - render json: list.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } }) - else - render json: list.errors, status: :unprocessable_entity - end - end - - def update - service = Boards::Lists::MoveService.new(project, current_user, move_params) - - if service.execute - head :ok - else - head :unprocessable_entity - end - end - - def destroy - service = Boards::Lists::DestroyService.new(project, current_user, params) - - if service.execute - head :ok - else - head :unprocessable_entity - end - end - - def generate - service = Boards::Lists::GenerateService.new(project, current_user) - - if service.execute - render json: project.board.lists.label.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } }) - else - head :unprocessable_entity - end - end - - private - - def authorize_admin_list! - return render_403 unless can?(current_user, :admin_list, project) - end - - def list_params - params.require(:list).permit(:label_id) - end - - def move_params - params.require(:list).permit(:position).merge(id: params[:id]) - end - - def record_not_found(exception) - render json: { error: exception.message }, status: :not_found - end -end diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index 095af6c35eb..71eb56aed0b 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -16,9 +16,8 @@ module Projects end def create - list = project.board.lists.find(params[:list_id]) service = ::Boards::Issues::CreateService.new(project, current_user, issue_params) - issue = service.execute(list) + issue = service.execute if issue.valid? render json: serialize_as_json(issue) @@ -60,15 +59,15 @@ module Projects end def filter_params - params.merge(id: params[:list_id]) + params.merge(board_id: params[:board_id], id: params[:list_id]) end def move_params - params.permit(:id, :from_list_id, :to_list_id) + params.permit(:board_id, :id, :from_list_id, :to_list_id) end def issue_params - params.require(:issue).permit(:title).merge(request: request) + params.require(:issue).permit(:title).merge(board_id: params[:board_id], list_id: params[:list_id], request: request) end def serialize_as_json(resource) diff --git a/app/controllers/projects/boards/lists_controller.rb b/app/controllers/projects/boards/lists_controller.rb index b995f586737..76ae41319c4 100644 --- a/app/controllers/projects/boards/lists_controller.rb +++ b/app/controllers/projects/boards/lists_controller.rb @@ -5,11 +5,11 @@ module Projects before_action :authorize_read_list!, only: [:index] def index - render json: serialize_as_json(project.board.lists) + render json: serialize_as_json(board.lists) end def create - list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute + list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute(board) if list.valid? render json: serialize_as_json(list) @@ -19,7 +19,7 @@ module Projects end def update - list = project.board.lists.movable.find(params[:id]) + list = board.lists.movable.find(params[:id]) service = ::Boards::Lists::MoveService.new(project, current_user, move_params) if service.execute(list) @@ -30,8 +30,8 @@ module Projects end def destroy - list = project.board.lists.destroyable.find(params[:id]) - service = ::Boards::Lists::DestroyService.new(project, current_user, params) + list = board.lists.destroyable.find(params[:id]) + service = ::Boards::Lists::DestroyService.new(project, current_user) if service.execute(list) head :ok @@ -43,8 +43,8 @@ module Projects def generate service = ::Boards::Lists::GenerateService.new(project, current_user) - if service.execute - render json: serialize_as_json(project.board.lists.movable) + if service.execute(board) + render json: serialize_as_json(board.lists.movable) else head :unprocessable_entity end @@ -60,6 +60,10 @@ module Projects return render_403 unless can?(current_user, :read_list, project) end + def board + @board ||= project.boards.find(params[:board_id]) + end + def list_params params.require(:list).permit(:label_id) end diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 0035633b774..808affa4f98 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -1,12 +1,28 @@ class Projects::BoardsController < Projects::ApplicationController include IssuableCollections - - respond_to :html - before_action :authorize_read_board!, only: [:show] + before_action :authorize_read_board!, only: [:index, :show] + + def index + @boards = ::Boards::ListService.new(project, current_user).execute + + respond_to do |format| + format.html + format.json do + render json: serialize_as_json(@boards) + end + end + end def show - ::Boards::CreateService.new(project, current_user).execute + @board = project.boards.find(params[:id]) + + respond_to do |format| + format.html + format.json do + render json: serialize_as_json(@board) + end + end end private @@ -14,4 +30,8 @@ class Projects::BoardsController < Projects::ApplicationController def authorize_read_board! return access_denied! unless can?(current_user, :read_board, project) end + + def serialize_as_json(resource) + resource.as_json(only: [:id]) + end end diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 092ef32e6e3..923e7340e69 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -38,12 +38,12 @@ class Projects::GraphsController < Projects::ApplicationController @languages = @languages.map do |language| name, share = language - color = Digest::SHA256.hexdigest(name)[0...6] + color = Linguist::Language[name].color || "##{Digest::SHA256.hexdigest(name)[0...6]}" { value: (share.to_f * 100 / total).round(2), label: name, - color: "##{color}", - highlight: "##{color}" + color: color, + highlight: color } end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index ffd9833e3b1..9207c954335 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ :edit, :update, :show, :diffs, :commits, :conflicts, :builds, :pipelines, :merge, :merge_check, - :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts + :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues ] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines] before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :builds, :pipelines] @@ -31,6 +31,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController # Allow modify merge_request before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort] + before_action :authenticate_user!, only: [:assign_related_issues] + before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts] def index @@ -354,6 +356,25 @@ class Projects::MergeRequestsController < Projects::ApplicationController render layout: false end + def assign_related_issues + result = MergeRequests::AssignIssuesService.new(project, current_user, merge_request: @merge_request).execute + + respond_to do |format| + format.html do + case result[:count] + when 0 + flash[:error] = "Failed to assign you issues related to the merge request" + when 1 + flash[:notice] = "1 issue has been assigned to you" + else + flash[:notice] = "#{result[:count]} issues have been assigned to you" + end + + redirect_to(merge_request_path(@merge_request)) + end + end + end + def ci_status pipeline = @merge_request.pipeline if pipeline @@ -382,6 +403,30 @@ class Projects::MergeRequestsController < Projects::ApplicationController render json: response end + def ci_environments_status + environments = + begin + @merge_request.environments.map do |environment| + next unless can?(current_user, :read_environment, environment) + + project = environment.project + deployment = environment.first_deployment_for(@merge_request.diff_head_commit) + + { + id: environment.id, + name: environment.name, + url: namespace_project_environment_path(project.namespace, project, environment), + external_url: environment.external_url, + external_url_formatted: environment.formatted_external_url, + deployed_at: deployment.try(:created_at), + deployed_at_formatted: deployment.try(:formatted_deployment_time) + } + end.compact + end + + render json: environments + end + protected def selected_target_project diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 6ea8ee62bc5..8fea20cefef 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -20,6 +20,8 @@ class Projects::TagsController < Projects::ApplicationController def show @tag = @repository.find_tag(params[:id]) + return render_404 unless @tag + @release = @project.releases.find_or_initialize_by(tag: @tag.name) @commit = @repository.commit(@tag.target) end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index d198782138a..dee57e4a388 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,10 +1,10 @@ class SnippetsController < ApplicationController include ToggleAwardEmoji - before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] # Allow read snippet - before_action :authorize_read_snippet!, only: [:show, :raw] + before_action :authorize_read_snippet!, only: [:show, :raw, :download] # Allow modify snippet before_action :authorize_update_snippet!, only: [:edit, :update] @@ -12,7 +12,7 @@ class SnippetsController < ApplicationController # Allow destroy snippet before_action :authorize_admin_snippet!, only: [:destroy] - skip_before_action :authenticate_user!, only: [:index, :show, :raw] + skip_before_action :authenticate_user!, only: [:index, :show, :raw, :download] layout 'snippets' respond_to :html @@ -75,6 +75,14 @@ class SnippetsController < ApplicationController ) end + def download + send_data( + @snippet.content, + type: 'text/plain; charset=utf-8', + filename: @snippet.sanitized_file_name + ) + end + protected def snippet diff --git a/app/finders/trending_projects_finder.rb b/app/finders/trending_projects_finder.rb deleted file mode 100644 index c1e434d9926..00000000000 --- a/app/finders/trending_projects_finder.rb +++ /dev/null @@ -1,16 +0,0 @@ -# Finder for retrieving public trending projects in a given time range. -class TrendingProjectsFinder - # current_user - The currently logged in User, if any. - # last_months - The number of months to limit the trending data to. - def execute(months_limit = 1) - Rails.cache.fetch(cache_key_for(months_limit), expires_in: 1.day) do - Project.public_only.trending(months_limit.months.ago) - end - end - - private - - def cache_key_for(months) - "trending_projects/#{months}" - end -end diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb new file mode 100644 index 00000000000..b7247ffa8b2 --- /dev/null +++ b/app/helpers/boards_helper.rb @@ -0,0 +1,12 @@ +module BoardsHelper + def board_data + board = @board || @boards.first + + { + endpoint: namespace_project_boards_path(@project.namespace, @project), + board_id: board.id, + disabled: !can?(current_user, :admin_list, @project), + issue_link_base: namespace_project_issues_path(@project.namespace, @project) + } + end +end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index b478580978b..85e1dc33ee8 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -15,13 +15,14 @@ module ButtonHelper # # See http://clipboardjs.com/#usage def clipboard_button(data = {}) + css_class = data[:class] || 'btn-clipboard btn-transparent' data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) content_tag :button, icon('clipboard'), - class: "btn btn-clipboard", + class: "btn #{css_class}", data: data, type: :button, - title: "Copy to Clipboard" + title: 'Copy to Clipboard' end def http_clone_button(project, placement = 'right', append_link: true) diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 8abe7865fed..249cb44e9d5 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -72,6 +72,19 @@ module MergeRequestsHelper ) end + def mr_assign_issues_link + issues = MergeRequests::AssignIssuesService.new(@project, + current_user, + merge_request: @merge_request, + closes_issues: mr_closes_issues + ).assignable_issues + path = assign_related_issues_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) + if issues.present? + pluralize_this_issue = issues.count > 1 ? "these issues" : "this issue" + link_to "Assign yourself to #{pluralize_this_issue}", path, method: :post + end + 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)) @@ -110,4 +123,8 @@ module MergeRequestsHelper def version_index(merge_request_diff) @merge_request_diffs.size - @merge_request_diffs.index(merge_request_diff) end + + def different_base?(version1, version2) + version1 && version2 && version1.base_commit_sha != version2.base_commit_sha + end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index bf2d861b86f..042b1c04054 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -3,6 +3,7 @@ module Ci extend Ci::Model include HasStatus include Importable + include AfterCommitQueue self.table_name = 'ci_commits' @@ -56,6 +57,10 @@ module Ci pipeline.finished_at = Time.now end + before_transition do |pipeline| + pipeline.update_duration + end + after_transition [:created, :pending] => :running do |pipeline| MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)). update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil) @@ -66,8 +71,8 @@ module Ci update_all(latest_build_finished_at: pipeline.finished_at) end - before_transition do |pipeline| - pipeline.update_duration + after_transition [:created, :pending, :running] => :success do |pipeline| + pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) } end after_transition do |pipeline, transition| @@ -292,9 +297,9 @@ module Ci # Merge requests for which the current pipeline is running against # the merge request's latest commit. def merge_requests - @merge_requests ||= - project.merge_requests.where(source_branch: ref). - select { |merge_request| merge_request.pipeline.try(:id) == id } + @merge_requests ||= project.merge_requests + .where(source_branch: self.ref) + .select { |merge_request| merge_request.pipeline.try(:id) == self.id } end def merge_requests_with_active_first diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 9fa8d17e74e..7b554be4f9a 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -1,6 +1,7 @@ class CommitStatus < ActiveRecord::Base include HasStatus include Importable + include AfterCommitQueue self.table_name = 'ci_builds' @@ -85,25 +86,24 @@ class CommitStatus < ActiveRecord::Base end after_transition do |commit_status, transition| - commit_status.pipeline.try do |pipeline| - break if transition.loopback? - - if commit_status.complete? - ProcessPipelineWorker.perform_async(pipeline.id) + next if transition.loopback? + + commit_status.run_after_commit do + pipeline.try do |pipeline| + if complete? + PipelineProcessWorker.perform_async(pipeline.id) + else + PipelineUpdateWorker.perform_async(pipeline.id) + end end - - UpdatePipelineWorker.perform_async(pipeline.id) end - - true - end - - after_transition [:created, :pending, :running] => :success do |commit_status| - MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status) end after_transition any => :failed do |commit_status| - MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status) + commit_status.run_after_commit do + MergeRequests::AddTodoWhenBuildFailsService + .new(pipeline.project, nil).execute(self) + end end end diff --git a/app/models/compare.rb b/app/models/compare.rb index 4856510f526..3a8bbcb1acd 100644 --- a/app/models/compare.rb +++ b/app/models/compare.rb @@ -11,9 +11,10 @@ class Compare end end - def initialize(compare, project) + def initialize(compare, project, straight: false) @compare = compare @project = project + @straight = straight end def commits @@ -45,6 +46,18 @@ class Compare end end + def start_commit_sha + start_commit.try(:sha) + end + + def base_commit_sha + base_commit.try(:sha) + end + + def head_commit_sha + commit.try(:sha) + end + def raw_diffs(*args) @compare.diffs(*args) end @@ -58,9 +71,9 @@ class Compare def diff_refs Gitlab::Diff::DiffRefs.new( - base_sha: base_commit.try(:sha), - start_sha: start_commit.try(:sha), - head_sha: commit.try(:sha) + base_sha: @straight ? start_commit_sha : base_commit_sha, + start_sha: start_commit_sha, + head_sha: head_commit_sha ) end end diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index be295487fd2..8ed4a56b19b 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -2,6 +2,8 @@ class CycleAnalytics include Gitlab::Database::Median include Gitlab::Database::DateTime + DEPLOYMENT_METRIC_STAGES = %i[production staging] + def initialize(project, from:) @project = project @from = from @@ -66,7 +68,7 @@ class CycleAnalytics # cycle analytics stage. interval_query = Arel::Nodes::As.new( cte_table, - subtract_datetimes(base_query, end_time_attrs, start_time_attrs, name.to_s)) + subtract_datetimes(base_query_for(name), end_time_attrs, start_time_attrs, name.to_s)) median_datetime(cte_table, interval_query, name) end @@ -75,7 +77,7 @@ class CycleAnalytics # closes the given issue) with issue and merge request metrics included. The metrics # are loaded with an inner join, so issues / merge requests without metrics are # automatically excluded. - def base_query + def base_query_for(name) arel_table = MergeRequestsClosingIssues.arel_table # Load issues @@ -91,7 +93,11 @@ class CycleAnalytics join(MergeRequest::Metrics.arel_table). on(MergeRequest.arel_table[:id].eq(MergeRequest::Metrics.arel_table[:merge_request_id])) - # Limit to merge requests that have been deployed to production after `@from` - query.where(MergeRequest::Metrics.arel_table[:first_deployed_to_production_at].gteq(@from)) + if DEPLOYMENT_METRIC_STAGES.include?(name) + # Limit to merge requests that have been deployed to production after `@from` + query.where(MergeRequest::Metrics.arel_table[:first_deployed_to_production_at].gteq(@from)) + end + + query end end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 82b27b78229..3d9902d496e 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -40,7 +40,14 @@ class Deployment < ActiveRecord::Base def includes_commit?(commit) return false unless commit - project.repository.is_ancestor?(commit.id, sha) + # Before 8.10, deployments didn't have keep-around refs. Any deployment + # created before then could have a `sha` referring to a commit that no + # longer exists in the repository, so just ignore those. + begin + project.repository.is_ancestor?(commit.id, sha) + rescue Rugged::OdbError + false + end end def update_merge_request_metrics! @@ -77,6 +84,10 @@ class Deployment < ActiveRecord::Base take end + def formatted_deployment_time + created_at.to_time.in_time_zone.to_s(:medium) + end + private def ref_path diff --git a/app/models/environment.rb b/app/models/environment.rb index f0f3ee23223..d970bc0a005 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -48,7 +48,22 @@ class Environment < ActiveRecord::Base self.name == "production" end + def first_deployment_for(commit) + ref = project.repository.ref_name_for_sha(ref_path, commit.sha) + + return nil unless ref + + deployment_id = ref.split('/').last + deployments.find(deployment_id) + end + def ref_path "refs/environments/#{Shellwords.shellescape(name)}" end + + def formatted_external_url + return nil unless external_url + + external_url.gsub(/\A.*?:\/\//, '') + end end diff --git a/app/models/event.rb b/app/models/event.rb index 314d5ba438f..0764cb8cabd 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -68,8 +68,10 @@ class Event < ActiveRecord::Base true elsif issue? || issue_note? Ability.allowed?(user, :read_issue, note? ? note_target : target) + elsif merge_request? || merge_request_note? + Ability.allowed?(user, :read_merge_request, note? ? note_target : target) else - ((merge_request? || note?) && target.present?) || milestone? + milestone? end end @@ -280,6 +282,10 @@ class Event < ActiveRecord::Base note? && target && target.for_issue? end + def merge_request_note? + note? && target && target.for_merge_request? + end + def project_snippet_note? target.for_snippet? end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 852317bbed1..3efb7aead77 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -692,12 +692,15 @@ class MergeRequest < ActiveRecord::Base def environments return [] unless diff_head_commit - 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 + @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 + end end def state_human_name diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 36b8b70870b..b8a10b7968e 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -6,6 +6,9 @@ class MergeRequestDiff < ActiveRecord::Base # Prevent store of diff if commits amount more then 500 COMMITS_SAFE_SIZE = 100 + # Valid types of serialized diffs allowed by Gitlab::Git::Diff + VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta] + belongs_to :merge_request state_machine :state, initial: :empty do @@ -164,12 +167,24 @@ class MergeRequestDiff < ActiveRecord::Base self == merge_request.merge_request_diff end - def compare_with(sha) - CompareService.new.execute(project, head_commit_sha, project, sha) + def compare_with(sha, straight: true) + # When compare merge request versions we want diff A..B instead of A...B + # so we handle cases when user does squash and rebase of the commits between versions. + # For this reason we set straight to true by default. + CompareService.new.execute(project, head_commit_sha, project, sha, straight: straight) end private + # Old GitLab implementations may have generated diffs as ["--broken-diff"]. + # Avoid an error 500 by ignoring bad elements. See: + # https://gitlab.com/gitlab-org/gitlab-ce/issues/20776 + def valid_raw_diff?(raw) + return false unless raw.respond_to?(:each) + + raw.any? { |element| VALID_CLASSES.include?(element.class) } + end + def dump_commits(commits) commits.map(&:to_hash) end @@ -200,7 +215,7 @@ class MergeRequestDiff < ActiveRecord::Base end def load_diffs(raw, options) - if raw.respond_to?(:each) + if valid_raw_diff?(raw) if paths = options[:paths] raw = raw.select do |diff| paths.include?(diff[:old_path]) || paths.include?(diff[:new_path]) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index b7f2b2bbe61..b67049f0f55 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -61,15 +61,13 @@ class Namespace < ActiveRecord::Base def clean_path(path) path = path.dup # Get the email username by removing everything after an `@` sign. - path.gsub!(/@.*\z/, "") - # Usernames can't end in .git, so remove it. - path.gsub!(/\.git\z/, "") - # Remove dashes at the start of the username. - path.gsub!(/\A-+/, "") - # Remove periods at the end of the username. - path.gsub!(/\.+\z/, "") + path.gsub!(/@.*\z/, "") # Remove everything that's not in the list of allowed characters. - path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") + path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") + # Remove trailing violations ('.atom', '.git', or '.') + path.gsub!(/(\.atom|\.git|\.)*\z/, "") + # Remove leading violations ('-') + path.gsub!(/\A\-+/, "") # Users with the great usernames of "." or ".." would end up with a blank username. # Work around that by setting their username to "blank", followed by a counter. diff --git a/app/models/project.rb b/app/models/project.rb index a89ad9bca1d..9e68b0016e0 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -16,6 +16,9 @@ class Project < ActiveRecord::Base extend Gitlab::ConfigHelper + class BoardLimitExceeded < StandardError; end + + NUMBER_OF_PERMITTED_BOARDS = 1 UNKNOWN_IMPORT_URL = 'http://unknown.git' cache_markdown_field :description, pipeline: :description @@ -65,8 +68,7 @@ class Project < ActiveRecord::Base belongs_to :namespace has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id' - - has_one :board, dependent: :destroy + has_many :boards, before_add: :validate_board_limit, dependent: :destroy # Project services has_many :services @@ -376,19 +378,9 @@ class Project < ActiveRecord::Base %r{(?<project>#{name_pattern}/#{name_pattern})} end - def trending(since = 1.month.ago) - # By counting in the JOIN we don't expose the GROUP BY to the outer query. - # This means that calls such as "any?" and "count" just return a number of - # the total count, instead of the counts grouped per project as a Hash. - join_body = "INNER JOIN ( - SELECT project_id, COUNT(*) AS amount - FROM notes - WHERE created_at >= #{sanitize(since)} - AND system IS FALSE - GROUP BY project_id - ) join_note_counts ON projects.id = join_note_counts.project_id" - - joins(join_body).reorder('join_note_counts.amount DESC') + def trending + joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id'). + reorder('trending_projects.id ASC') end def cached_count @@ -838,11 +830,6 @@ class Project < ActiveRecord::Base end end - def update_merge_requests(oldrev, newrev, ref, user) - MergeRequests::RefreshService.new(self, user). - execute(oldrev, newrev, ref) - end - def valid_repo? repository.exists? rescue @@ -1350,4 +1337,8 @@ class Project < ActiveRecord::Base shared_projects.any? end + + def validate_board_limit(board) + raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS + end end diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb index 7613cbdea93..db46def11eb 100644 --- a/app/models/project_group_link.rb +++ b/app/models/project_group_link.rb @@ -10,7 +10,7 @@ class ProjectGroupLink < ActiveRecord::Base belongs_to :group validates :project_id, presence: true - validates :group_id, presence: true + validates :group, presence: true validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" } validates :group_access, presence: true validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true diff --git a/app/models/repository.rb b/app/models/repository.rb index bf59b74495b..72e473871fa 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -111,8 +111,10 @@ class Repository def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0) ref ||= root_ref - # Limited to 1000 commits for now, could be parameterized? - args = %W(#{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset} --max-count #{limit} --grep=#{query}) + args = %W( + #{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset} + --max-count #{limit} --grep=#{query} --regexp-ignore-case + ) args = args.concat(%W(-- #{path})) if path.present? git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp) @@ -717,6 +719,14 @@ class Repository end end + def ref_name_for_sha(ref_path, sha) + args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha}) + + # Not found -> ["", 0] + # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0] + Gitlab::Popen.popen(args, path_to_repo).first.split.last + end + def refs_contains_sha(ref_type, sha) args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha}) names = Gitlab::Popen.popen(args, path_to_repo).first @@ -1014,7 +1024,8 @@ class Repository root_ref_commit = commit(root_ref) if branch_commit - is_ancestor?(branch_commit.id, root_ref_commit.id) + same_head = branch_commit.id == root_ref_commit.id + !same_head && is_ancestor?(branch_commit.id, root_ref_commit.id) else nil end diff --git a/app/models/trending_project.rb b/app/models/trending_project.rb new file mode 100644 index 00000000000..27e3732da17 --- /dev/null +++ b/app/models/trending_project.rb @@ -0,0 +1,35 @@ +class TrendingProject < ActiveRecord::Base + belongs_to :project + + # The number of months to include in the trending calculation. + MONTHS_TO_INCLUDE = 1 + + # The maximum number of projects to include in the trending set. + PROJECTS_LIMIT = 100 + + # Populates the trending projects table with the current list of trending + # projects. + def self.refresh! + # The calculation **must** run in a transaction. If the removal of data and + # insertion of new data were to run separately a user might end up with an + # empty list of trending projects for a short period of time. + transaction do + delete_all + + timestamp = connection.quote(MONTHS_TO_INCLUDE.months.ago) + + connection.execute <<-EOF.strip_heredoc + INSERT INTO #{table_name} (project_id) + SELECT project_id + FROM notes + INNER JOIN projects ON projects.id = notes.project_id + WHERE notes.created_at >= #{timestamp} + AND notes.system IS FALSE + AND projects.visibility_level = #{Gitlab::VisibilityLevel::PUBLIC} + GROUP BY project_id + ORDER BY count(*) DESC + LIMIT #{PROJECTS_LIMIT}; + EOF + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 892ac28d5b3..f367f4616fb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -589,6 +589,11 @@ class User < ActiveRecord::Base end def set_projects_limit + # `User.select(:id)` raises + # `ActiveModel::MissingAttributeError: missing attribute: projects_limit` + # without this safeguard! + return unless self.has_attribute?(:projects_limit) + connection_default_value_defined = new_record? && !projects_limit_changed? return unless self.projects_limit.nil? || connection_default_value_defined diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index be25c750d67..be4721d7a51 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -40,7 +40,6 @@ class ProjectPolicy < BasePolicy can! :read_milestone can! :read_project_snippet can! :read_project_member - can! :read_merge_request can! :read_note can! :create_project can! :create_issue @@ -63,6 +62,7 @@ class ProjectPolicy < BasePolicy can! :read_pipeline can! :read_environment can! :read_deployment + can! :read_merge_request end # Permissions given when an user is team member of a project @@ -98,7 +98,6 @@ class ProjectPolicy < BasePolicy can! :admin_milestone can! :admin_project_snippet can! :admin_project_member - can! :admin_merge_request can! :admin_note can! :admin_wiki can! :admin_project @@ -118,6 +117,7 @@ class ProjectPolicy < BasePolicy can! :read_container_image can! :build_download_code can! :build_read_container_image + can! :read_merge_request end def owner_access! @@ -139,11 +139,18 @@ class ProjectPolicy < BasePolicy def team_access!(user) access = project.team.max_member_access(user.id) - guest_access! if access >= Gitlab::Access::GUEST - reporter_access! if access >= Gitlab::Access::REPORTER - team_member_reporter_access! if access >= Gitlab::Access::REPORTER - developer_access! if access >= Gitlab::Access::DEVELOPER - master_access! if access >= Gitlab::Access::MASTER + return if access < Gitlab::Access::GUEST + guest_access! + + return if access < Gitlab::Access::REPORTER + reporter_access! + team_member_reporter_access! + + return if access < Gitlab::Access::DEVELOPER + developer_access! + + return if access < Gitlab::Access::MASTER + master_access! end def archived_access! diff --git a/app/services/boards/base_service.rb b/app/services/boards/base_service.rb deleted file mode 100644 index b2069ca825a..00000000000 --- a/app/services/boards/base_service.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Boards - class BaseService < ::BaseService - delegate :board, to: :project - end -end diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb index 072a0749285..9bdd7b6f0cf 100644 --- a/app/services/boards/create_service.rb +++ b/app/services/boards/create_service.rb @@ -1,16 +1,21 @@ module Boards - class CreateService < Boards::BaseService + class CreateService < BaseService def execute - create_board! unless project.board.present? - project.board + if project.boards.empty? + create_board! + else + project.boards.first + end end private def create_board! - project.create_board - project.board.lists.create(list_type: :backlog) - project.board.lists.create(list_type: :done) + board = project.boards.create + board.lists.create(list_type: :backlog) + board.lists.create(list_type: :done) + + board end end end diff --git a/app/services/boards/issues/create_service.rb b/app/services/boards/issues/create_service.rb index 3701afd441f..c0d7ff5b585 100644 --- a/app/services/boards/issues/create_service.rb +++ b/app/services/boards/issues/create_service.rb @@ -1,14 +1,21 @@ module Boards module Issues - class CreateService < Boards::BaseService - def execute(list) - params.merge!(label_ids: [list.label_id]) - create_issue + class CreateService < BaseService + def execute + create_issue(params.merge(label_ids: [list.label_id])) end private - def create_issue + def board + @board ||= project.boards.find(params.delete(:board_id)) + end + + def list + @list ||= board.lists.find(params.delete(:list_id)) + end + + def create_issue(params) ::Issues::CreateService.new(project, current_user, params).execute end end diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 435a8c6e681..fd4a462c7b2 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -1,6 +1,6 @@ module Boards module Issues - class ListService < Boards::BaseService + class ListService < BaseService def execute issues = IssuesFinder.new(current_user, filter_params).execute issues = without_board_labels(issues) unless list.movable? @@ -10,6 +10,10 @@ module Boards private + def board + @board ||= project.boards.find(params[:board_id]) + end + def list @list ||= board.lists.find(params[:id]) end diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index 84dc3f70e76..96554a92a02 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -1,6 +1,6 @@ module Boards module Issues - class MoveService < Boards::BaseService + class MoveService < BaseService def execute(issue) return false unless can?(current_user, :update_issue, issue) return false unless valid_move? @@ -10,6 +10,10 @@ module Boards private + def board + @board ||= project.boards.find(params[:board_id]) + end + def valid_move? moving_from_list.present? && moving_to_list.present? && moving_from_list != moving_to_list @@ -49,7 +53,7 @@ module Boards if moving_to_list.movable? moving_from_list.label_id else - board.lists.movable.pluck(:label_id) + project.boards.joins(:lists).merge(List.movable).pluck(:label_id) end Array(label_ids).compact diff --git a/app/services/boards/list_service.rb b/app/services/boards/list_service.rb new file mode 100644 index 00000000000..84f1fc3a4e2 --- /dev/null +++ b/app/services/boards/list_service.rb @@ -0,0 +1,14 @@ +module Boards + class ListService < BaseService + def execute + create_board! if project.boards.empty? + project.boards + end + + private + + def create_board! + Boards::CreateService.new(project, current_user).execute + end + end +end diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb index b1887820bd4..abc7aeece39 100644 --- a/app/services/boards/lists/create_service.rb +++ b/app/services/boards/lists/create_service.rb @@ -1,23 +1,23 @@ module Boards module Lists - class CreateService < Boards::BaseService - def execute + class CreateService < BaseService + def execute(board) List.transaction do label = project.labels.find(params[:label_id]) - position = next_position + position = next_position(board) - create_list(label, position) + create_list(board, label, position) end end private - def next_position + def next_position(board) max_position = board.lists.movable.maximum(:position) max_position.nil? ? 0 : max_position.succ end - def create_list(label, position) + def create_list(board, label, position) board.lists.create(label: label, list_type: :label, position: position) end end diff --git a/app/services/boards/lists/destroy_service.rb b/app/services/boards/lists/destroy_service.rb index 25da3bfb56d..f986e05944c 100644 --- a/app/services/boards/lists/destroy_service.rb +++ b/app/services/boards/lists/destroy_service.rb @@ -1,9 +1,11 @@ module Boards module Lists - class DestroyService < Boards::BaseService + class DestroyService < BaseService def execute(list) return false unless list.destroyable? + @board = list.board + list.with_lock do decrement_higher_lists(list) remove_list(list) @@ -12,6 +14,8 @@ module Boards private + attr_reader :board + def decrement_higher_lists(list) board.lists.movable.where('position > ?', list.position) .update_all('position = position - 1') diff --git a/app/services/boards/lists/generate_service.rb b/app/services/boards/lists/generate_service.rb index 830e386c98b..d8048f1c67e 100644 --- a/app/services/boards/lists/generate_service.rb +++ b/app/services/boards/lists/generate_service.rb @@ -1,11 +1,11 @@ module Boards module Lists - class GenerateService < Boards::BaseService - def execute + class GenerateService < BaseService + def execute(board) return false unless board.lists.movable.empty? List.transaction do - label_params.each { |params| create_list(params) } + label_params.each { |params| create_list(board, params) } end true @@ -13,9 +13,9 @@ module Boards private - def create_list(params) + def create_list(board, params) label = find_or_create_label(params) - Lists::CreateService.new(project, current_user, label_id: label.id).execute + Lists::CreateService.new(project, current_user, label_id: label.id).execute(board) end def find_or_create_label(params) diff --git a/app/services/boards/lists/list_service.rb b/app/services/boards/lists/list_service.rb new file mode 100644 index 00000000000..c579ed4c869 --- /dev/null +++ b/app/services/boards/lists/list_service.rb @@ -0,0 +1,9 @@ +module Boards + module Lists + class ListService < BaseService + def execute(board) + board.lists + end + end + end +end diff --git a/app/services/boards/lists/move_service.rb b/app/services/boards/lists/move_service.rb index 020ff69f4a7..f2a68865f7b 100644 --- a/app/services/boards/lists/move_service.rb +++ b/app/services/boards/lists/move_service.rb @@ -1,7 +1,8 @@ module Boards module Lists - class MoveService < Boards::BaseService + class MoveService < BaseService def execute(list) + @board = list.board @old_position = list.position @new_position = params[:position] @@ -16,7 +17,7 @@ module Boards private - attr_reader :old_position, :new_position + attr_reader :board, :old_position, :new_position def valid_move? new_position.present? && new_position != old_position && diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 36c93dddadb..d3dd30b2588 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -16,6 +16,8 @@ module Ci process_stage(index) end + @pipeline.update_status + # Return a flag if a when builds got enqueued new_builds.flatten.any? end diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index 6d6075628af..5e8fafca98c 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -3,7 +3,7 @@ require 'securerandom' # Compare 2 branches for one repo or between repositories # and return Gitlab::Git::Compare object that responds to commits and diffs class CompareService - def execute(source_project, source_branch, target_project, target_branch) + def execute(source_project, source_branch, target_project, target_branch, straight: false) source_commit = source_project.commit(source_branch) return unless source_commit @@ -23,9 +23,10 @@ class CompareService raw_compare = Gitlab::Git::Compare.new( target_project.repository.raw_repository, target_branch, - source_sha + source_sha, + straight ) - Compare.new(raw_compare, target_project) + Compare.new(raw_compare, target_project, straight: straight) end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index c499427605a..e8415862de5 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -63,13 +63,12 @@ class GitPushService < BaseService protected def update_merge_requests - @project.update_merge_requests(params[:oldrev], params[:newrev], params[:ref], current_user) + UpdateMergeRequestsWorker.perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref]) EventCreateService.new.push(@project, current_user, build_push_data) - SystemHooksService.new.execute_hooks(build_push_data_system_hook.dup, :push_hooks) @project.execute_hooks(build_push_data.dup, :push_hooks) @project.execute_services(build_push_data.dup, :push_hooks) - Ci::CreatePipelineService.new(project, current_user, build_push_data).execute + Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute ProjectCacheWorker.perform_async(@project.id) end @@ -148,16 +147,6 @@ class GitPushService < BaseService push_commits) end - def build_push_data_system_hook - @push_data_system ||= Gitlab::DataBuilder::Push.build( - @project, - current_user, - params[:oldrev], - params[:newrev], - params[:ref], - []) - end - def push_to_existing_branch? # Return if this is not a push to a branch (e.g. new commits) Gitlab::Git.branch_ref?(params[:ref]) && !Gitlab::Git.blank_ref?(params[:oldrev]) diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb index 566049525cb..d572a928a42 100644 --- a/app/services/merge_requests/add_todo_when_build_fails_service.rb +++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb @@ -2,14 +2,14 @@ module MergeRequests class AddTodoWhenBuildFailsService < MergeRequests::BaseService # Adds a todo to the parent merge_request when a CI build fails def execute(commit_status) - each_merge_request(commit_status) do |merge_request| + commit_status_merge_requests(commit_status) do |merge_request| todo_service.merge_request_build_failed(merge_request) end end # Closes any pending build failed todos for the parent MRs when a build is retried def close(commit_status) - each_merge_request(commit_status) do |merge_request| + commit_status_merge_requests(commit_status) do |merge_request| todo_service.merge_request_build_retried(merge_request) end end diff --git a/app/services/merge_requests/assign_issues_service.rb b/app/services/merge_requests/assign_issues_service.rb new file mode 100644 index 00000000000..f636e5fec4f --- /dev/null +++ b/app/services/merge_requests/assign_issues_service.rb @@ -0,0 +1,35 @@ +module MergeRequests + class AssignIssuesService < BaseService + def assignable_issues + @assignable_issues ||= begin + if current_user == merge_request.author + closes_issues.select do |issue| + !issue.assignee_id? && can?(current_user, :admin_issue, issue) + end + else + [] + end + end + end + + def execute + assignable_issues.each do |issue| + Issues::UpdateService.new(issue.project, current_user, assignee_id: current_user.id).execute(issue) + end + + { + count: assignable_issues.count + } + end + + private + + def merge_request + params[:merge_request] + end + + def closes_issues + @closes_issues ||= params[:closes_issues] || merge_request.closes_issues(current_user) + end + end +end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index d0d155b7ee1..58f69a41e14 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -42,28 +42,33 @@ module MergeRequests super(:merge_request) end - def merge_request_from(commit_status) - branches = commit_status.ref + def merge_requests_for(branch) + origin_merge_requests = @project.origin_merge_requests + .opened.where(source_branch: branch).to_a - # This is for ref-less builds - branches ||= @project.repository.branch_names_contains(commit_status.sha) + fork_merge_requests = @project.fork_merge_requests + .opened.where(source_branch: branch).to_a - return [] if branches.blank? + (origin_merge_requests + fork_merge_requests) + .uniq.select(&:source_project) + end - merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a - merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a + def pipeline_merge_requests(pipeline) + merge_requests_for(pipeline.ref).each do |merge_request| + next unless pipeline == merge_request.pipeline - merge_requests.uniq.select(&:source_project) + yield merge_request + end end - def each_merge_request(commit_status) - merge_request_from(commit_status).each do |merge_request| + def commit_status_merge_requests(commit_status) + merge_requests_for(commit_status.ref).each do |merge_request| pipeline = merge_request.pipeline next unless pipeline next unless pipeline.sha == commit_status.sha - yield merge_request, pipeline + yield merge_request end end end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index e57791f6818..404f75616b5 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -4,7 +4,7 @@ module MergeRequests merge_request = MergeRequest.new(params) # Set MR attributes - merge_request.can_be_created = false + merge_request.can_be_created = true merge_request.compare_commits = [] merge_request.source_project = project unless merge_request.source_project @@ -22,6 +22,12 @@ module MergeRequests return build_failed(merge_request, message) end + if merge_request.source_project == merge_request.target_project && + merge_request.target_branch == merge_request.source_branch + + return build_failed(merge_request, 'You must select different branches') + end + compare = CompareService.new.execute( merge_request.source_project, merge_request.source_branch, @@ -29,17 +35,8 @@ module MergeRequests merge_request.target_branch, ) - commits = compare.commits - - # At this point we decide if merge request can be created - # If we have at least one commit to merge -> creation allowed - if commits.present? - merge_request.compare_commits = commits - merge_request.can_be_created = true - merge_request.compare = compare - else - merge_request.can_be_created = false - end + merge_request.compare_commits = compare.commits + merge_request.compare = compare set_title_and_description(merge_request) end @@ -89,6 +86,8 @@ module MergeRequests end end + merge_request.title = merge_request.wip_title if commits.empty? + merge_request end diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb index 4ad5fb08311..dc159de0058 100644 --- a/app/services/merge_requests/merge_when_build_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb @@ -18,12 +18,13 @@ module MergeRequests merge_request.save end - # Triggers the automatic merge of merge_request once the build succeeds - def trigger(commit_status) - each_merge_request(commit_status) do |merge_request, pipeline| + # Triggers the automatic merge of merge_request once the pipeline succeeds + def trigger(pipeline) + return unless pipeline.success? + + pipeline_merge_requests(pipeline) do |merge_request| next unless merge_request.merge_when_build_succeeds? next unless merge_request.mergeable? - next unless pipeline.success? MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params) end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 9dbec49d163..a37cc3fdf21 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -15,7 +15,10 @@ module MergeRequests params.except!(:target_branch, :force_remove_source_branch) end - merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) + if params[:force_remove_source_branch].present? + merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) + end + handle_wip_event(merge_request) update(merge_request) end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index de8049b8e2e..72712afc07e 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -475,10 +475,12 @@ class NotificationService end def reject_users_without_access(recipients, target) - return recipients unless target.is_a?(Issue) + return recipients unless target.is_a?(Issuable) + + ability = :"read_#{target.to_ability_name}" recipients.select do |user| - user.can?(:read_issue, target) + user.can?(ability, target) end end diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index 1725a30fae5..e4ae3dec8aa 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -122,7 +122,12 @@ module SlashCommands command :label do |labels_param| label_ids = find_label_ids(labels_param) - @updates[:add_label_ids] = label_ids unless label_ids.empty? + if label_ids.any? + @updates[:add_label_ids] ||= [] + @updates[:add_label_ids] += label_ids + + @updates[:add_label_ids].uniq! + end end desc 'Remove all or specific label(s)' @@ -136,7 +141,12 @@ module SlashCommands if labels_param.present? label_ids = find_label_ids(labels_param) - @updates[:remove_label_ids] = label_ids unless label_ids.empty? + if label_ids.any? + @updates[:remove_label_ids] ||= [] + @updates[:remove_label_ids] += label_ids + + @updates[:remove_label_ids].uniq! + end else @updates[:label_ids] = [] end @@ -152,7 +162,12 @@ module SlashCommands command :relabel do |labels_param| label_ids = find_label_ids(labels_param) - @updates[:label_ids] = label_ids unless label_ids.empty? + if label_ids.any? + @updates[:label_ids] ||= [] + @updates[:label_ids] += label_ids + + @updates[:label_ids].uniq! + end end desc 'Add a todo' diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 776530ac0a5..f8e6b2ef094 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -273,12 +273,12 @@ class TodoService end def reject_users_without_access(users, project, target) - if target.is_a?(Note) && target.for_issue? + if target.is_a?(Note) && (target.for_issue? || target.for_merge_request?) target = target.noteable end - if target.is_a?(Issue) - select_users(users, :read_issue, target) + if target.is_a?(Issuable) + select_users(users, :"read_#{target.to_ability_name}", target) else select_users(users, :read_project, project) end diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb index 4dc3b2ab9a0..2821ecf0a88 100644 --- a/app/validators/namespace_validator.rb +++ b/app/validators/namespace_validator.rb @@ -24,6 +24,7 @@ class NamespaceValidator < ActiveModel::EachValidator projects public repository + robots.txt s search services diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 0d79ca7dc52..c4c68cd7891 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -221,7 +221,11 @@ %fieldset %legend Metrics %p - These settings require a restart to take effect. + Setup InfluxDB to measure a wide variety of statistics like the time spent + in running SQL queries. These settings require a + = link_to 'restart', help_page_path('administration/restart_gitlab') + to take effect. + = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/introduction') .form-group .col-sm-offset-2.col-sm-10 .checkbox diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index e44a2bfed9d..99a58bbb676 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -116,4 +116,4 @@ -# Shortcut to issue boards %li.hidden - = link_to 'Issue Boards', namespace_project_board_path(@project.namespace, @project), title: 'Issue Boards', class: 'shortcuts-issue-boards' + = link_to 'Issue Boards', namespace_project_boards_path(@project.namespace, @project), title: 'Issue Boards', class: 'shortcuts-issue-boards' diff --git a/app/views/projects/boards/index.html.haml b/app/views/projects/boards/index.html.haml new file mode 100644 index 00000000000..885f8e34b55 --- /dev/null +++ b/app/views/projects/boards/index.html.haml @@ -0,0 +1,16 @@ +- @no_container = true +- @content_class = "issue-boards-content" +- page_title "Boards" + +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('boards/boards_bundle.js') + = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test? + += render "projects/issues/head" + += render 'shared/issuable/filter', type: :boards + +.boards-list#board-app{ "v-cloak" => true, data: board_data } + .boards-app-loading.text-center{ "v-if" => "loading" } + = icon("spinner spin") + = render "projects/boards/components/board" diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml index edbbd3f3d2a..885f8e34b55 100644 --- a/app/views/projects/boards/show.html.haml +++ b/app/views/projects/boards/show.html.haml @@ -10,10 +10,7 @@ = render 'shared/issuable/filter', type: :boards -.boards-list#board-app{ "v-cloak" => true, - "data-endpoint" => "#{namespace_project_board_path(@project.namespace, @project)}", - "data-disabled" => "#{!can?(current_user, :admin_list, @project)}", - "data-issue-link-base" => "#{namespace_project_issues_path(@project.namespace, @project)}" } +.boards-list#board-app{ "v-cloak" => true, data: board_data } .boards-app-loading.text-center{ "v-if" => "loading" } = icon("spinner spin") = render "projects/boards/components/board" diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 5217b8bf028..4480b2f22c3 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -30,8 +30,8 @@ = render 'projects/buttons/download', project: @project, ref: branch.name - - if can_remove_branch?(@project, branch.name) - = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do + - if can?(current_user, :push_code, @project) + = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: "btn btn-remove remove-row has-tooltip #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}", title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do = icon("trash-o") - if branch.name != @repository.root_ref diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index f5344091cae..966633f1f89 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -128,7 +128,7 @@ - builds.select{|build| build.status == build_status}.each do |build| .build-job{class: ('active' if build == @build), data: {stage: build.stage}} = link_to namespace_project_build_path(@project.namespace, @project, build) do - = icon('right-arrow') + = icon('arrow-right') = ci_icon_for_status(build.status) %span - if build.name diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml index 547bc0c9c19..017d3ff6af2 100644 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ b/app/views/projects/ci/builds/_build_pipeline.html.haml @@ -5,8 +5,10 @@ .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 - = render_status_with_link('build', subject.status) + %span.ci-status-icon + = render_status_with_link('build', subject.status) .ci-status-text= subject.name - else - = render_status_with_link('build', subject.status) + %span.ci-status-icon + = render_status_with_link('build', subject.status) = ci_icon_for_status(subject.status) diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index da5b9832ba5..288c06d9b67 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -1,45 +1,46 @@ -.row-content-block.build-content.middle-block.pipeline-actions - .pull-right - .btn.btn-grouped.btn-white.toggle-pipeline-btn - %span.toggle-btn-text Hide - %span pipeline graph - = icon('caret-up') - - if can?(current_user, :update_pipeline, pipeline.project) - - if pipeline.builds.latest.failed.any?(&:retryable?) - = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post +.pipeline-graph-container + .row-content-block.build-content.middle-block.pipeline-actions + .pull-right + .btn.btn-grouped.btn-white.toggle-pipeline-btn + %span.toggle-btn-text Hide + %span pipeline graph + %span.caret + - if can?(current_user, :update_pipeline, pipeline.project) + - if pipeline.builds.latest.failed.any?(&:retryable?) + = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post - - if pipeline.builds.running_or_pending.any? - = link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post + - if pipeline.builds.running_or_pending.any? + = link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post - .oneline.clearfix - - if defined?(pipeline_details) && pipeline_details - Pipeline - = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace" - with - = pluralize pipeline.statuses.count(:id), "build" - - if pipeline.ref - for - = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace" - - if defined?(link_to_commit) && link_to_commit - for commit - = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace" - - if pipeline.duration - in - = time_interval_in_words pipeline.duration + .oneline.clearfix + - if defined?(pipeline_details) && pipeline_details + Pipeline + = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace" + with + = pluralize pipeline.statuses.count(:id), "build" + - if pipeline.ref + for + = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace" + - if defined?(link_to_commit) && link_to_commit + for commit + = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace" + - if pipeline.duration + in + = time_interval_in_words pipeline.duration -.row-content-block.build-content.middle-block.pipeline-graph - .pipeline-visualization - %ul.stage-column-list - - stages = pipeline.stages_with_latest_statuses - - stages.each do |stage, statuses| - %li.stage-column - .stage-name - %a{name: stage} - - if stage - = stage.titleize - .builds-container - %ul - = render "projects/commit/pipeline_stage", statuses: statuses + .row-content-block.build-content.middle-block.pipeline-graph.hidden + .pipeline-visualization + %ul.stage-column-list + - stages = pipeline.stages_with_latest_statuses + - stages.each do |stage, statuses| + %li.stage-column + .stage-name + %a{name: stage} + - if stage + = stage.titleize + .builds-container + %ul + = render "projects/commit/pipeline_stage", statuses: statuses - if pipeline.yaml_errors.present? diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml index 23c5c51fbc2..289aa5178b1 100644 --- a/app/views/projects/commit/_pipeline_stage.html.haml +++ b/app/views/projects/commit/_pipeline_stage.html.haml @@ -10,5 +10,5 @@ - else %li.build .curve - .build-content + .dropdown.inline.build-content = render "projects/commit/pipeline_status_group", name: group_name, subject: grouped_statuses diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml index 4e7a6f1af08..5d0d5ba0262 100644 --- a/app/views/projects/commit/_pipeline_status_group.html.haml +++ b/app/views/projects/commit/_pipeline_status_group.html.haml @@ -1,11 +1,13 @@ - group_status = CommitStatus.where(id: subject).status -= render_status_with_link('build', group_status) -.dropdown.inline - %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } - %span.ci-status-text - = name - %span.badge= subject.size - %ul.dropdown-menu.grouped-pipeline-dropdown - .arrow +%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } + %span.ci-status-icon + = render_status_with_link('build', group_status) + %span.ci-status-text + = name + %span.badge= subject.size +.dropdown-menu.grouped-pipeline-dropdown + .arrow + %ul - subject.each do |status| - = render "projects/#{status.to_partial_path}_pipeline", subject: status + %li + = render "projects/#{status.to_partial_path}_pipeline", subject: status diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index e9ff8e90dd5..45be6581cfc 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -4,7 +4,7 @@ %div{ class: container_class } .sub-header-block - Compare branches, tags or commit ranges. + Compare Git revisions. %br Fill input field with commit id like %code.label-branch 4eedf23 diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index d07de45fdde..257e0a855bd 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -8,7 +8,7 @@ = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file", disabled: @diff_notes_disabled do = icon('comment') \ - + = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-file-option') - if editable_diff?(diff_file) - link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {} = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index d19422c8657..c8f84b96cb7 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -91,7 +91,7 @@ Git Large File Storage = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') .col-md-3 - = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control' + = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control', data: { field: 'lfs_enabled' } - if Gitlab.config.registry.enabled .form-group diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 331dc1fcc29..80fe6be49b0 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -62,5 +62,3 @@ %td.coverage - if generic_commit_status.try(:coverage) #{generic_commit_status.coverage}% - - %td 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 409f4701e4b..0a66d60accc 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,7 +1,9 @@ - if subject.target_url = link_to subject.target_url do - = render_status_with_link('commit status', subject.status) + %span.ci-status-icon + = render_status_with_link('commit status', subject.status) %span.ci-status-text= subject.name - else - = render_status_with_link('commit status', subject.status) + %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/_head.html.haml b/app/views/projects/issues/_head.html.haml index 509b01c548a..4825820c4d9 100644 --- a/app/views/projects/issues/_head.html.haml +++ b/app/views/projects/issues/_head.html.haml @@ -10,7 +10,7 @@ Issues = nav_link(controller: :boards) do - = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do + = link_to namespace_project_boards_path(@project.namespace, @project), title: 'Board' do %span Board diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml index 7cf1923456e..3a6fbbc7fbc 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.title} (##{@issue.iid})", "Issues" +- page_title "Edit", "#{@issue.to_reference} #{@issue.title}", "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 b94d6f8633c..09347ad5fff 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,4 +1,4 @@ -- page_title "#{@issue.title} (##{@issue.iid})", "Issues" +- page_title "#{@issue.to_reference} #{@issue.title}", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes @@ -23,8 +23,8 @@ .issuable-actions .clearfix.issue-btn-group.dropdown %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } - = icon('caret-down') Options + = icon('caret-down') .dropdown-menu.dropdown-menu-align-right.hidden-lg %ul - if can?(current_user, :create_issue, @project) diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index de39964fca8..466ec1475d8 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -65,19 +65,6 @@ - if @merge_request.errors.any? = form_errors(@merge_request) - - elsif @merge_request.source_branch.present? && @merge_request.target_branch.present? - .light-well.append-bottom-default - .center - %h4 - There isn't anything to merge. - %p.slead - - if @merge_request.source_branch == @merge_request.target_branch - You'll need to use different branch names to get a valid comparison. - - else - %span.label-branch #{@merge_request.source_branch} - and - %span.label-branch #{@merge_request.target_branch} - are the same. = f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn" :javascript diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 88d8013a0d1..da6927879a4 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -18,29 +18,35 @@ = f.hidden_field :target_branch .mr-compare.merge-request - %ul.merge-request-tabs.nav-links.no-top.no-bottom - %li.commits-tab.active - = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do - Commits - %span.badge= @commits.size - - if @pipeline - %li.builds-tab - = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do - Builds - %span.badge= @statuses.size - %li.diffs-tab - = link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do - Changes - %span.badge= @merge_request.diff_size + - if @commits.empty? + .commits-empty + %h4 + There are no commits yet. + = custom_icon ('illustration_no_commits') + - else + %ul.merge-request-tabs.nav-links.no-top.no-bottom + %li.commits-tab.active + = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do + Commits + %span.badge= @commits.size + - if @pipeline + %li.builds-tab + = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do + Builds + %span.badge= @statuses.size + %li.diffs-tab + = link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do + Changes + %span.badge= @merge_request.diff_size - .tab-content - #commits.commits.tab-pane.active - = render "projects/merge_requests/show/commits" - #diffs.diffs.tab-pane - - # This tab is always loaded via AJAX - - if @pipeline - #builds.builds.tab-pane - = render "projects/merge_requests/show/builds" + .tab-content + #commits.commits.tab-pane.active + = render "projects/merge_requests/show/commits" + #diffs.diffs.tab-pane + - # This tab is always loaded via AJAX + - if @pipeline + #builds.builds.tab-pane + = render "projects/merge_requests/show/builds" .mr-loading-status = spinner diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 46a2d862c91..47dd51639b5 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.title} (#{@merge_request.to_reference})", "Merge Requests" +- page_title "#{@merge_request.to_reference} #{@merge_request.title}", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - content_for :page_specific_javascripts do diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml index 03159f123f3..7c3ac6652ee 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.title} (#{@merge_request.to_reference}", "Merge Requests" +- page_title "Edit", "#{@merge_request.to_reference} #{@merge_request.title}", "Merge Requests" %h3.page-title Edit Merge Request #{@merge_request.to_reference} diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 9f2a0f5d99a..e7c5bca6a37 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -19,8 +19,8 @@ .issuable-actions .clearfix.issue-btn-group.dropdown %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } - = icon('caret-down') Options + = icon('caret-down') .dropdown-menu.dropdown-menu-align-right.hidden-lg %ul %li{ class: merge_request_button_visibility(@merge_request, true) } diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml index 988ac0feae1..eab48b78cb3 100644 --- a/app/views/projects/merge_requests/show/_versions.html.haml +++ b/app/views/projects/merge_requests/show/_versions.html.haml @@ -64,6 +64,16 @@ #{@merge_request.target_branch} (base) .monospace #{short_sha(@merge_request_diff.base_commit_sha)} + - if different_base?(@start_version, @merge_request_diff) + .content-block + = icon('info-circle') + Selected versions have different base commits. + Changes will include + = link_to namespace_project_compare_path(@project.namespace, @project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do + new commits + from + %code #{@merge_request.target_branch} + - unless @merge_request_diff.latest? && !@start_sha .comments-disabled-notif.content-block = icon('info-circle') diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 5b7f83c344f..a82c846baa7 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -44,17 +44,5 @@ = icon("times-circle") Could not connect to the CI server. Please check your settings and try again. -- @merge_request.environments.sort_by(&:name).each do |environment| - - if can?(current_user, :read_environment, environment) - .mr-widget-heading - .ci_widget.ci-success - = ci_icon_for_status("success") - %span - Deployed to - = succeed '.' do - = link_to environment.name, environment_path(environment), class: 'environment' - - external_url = environment.external_url - - if external_url - = link_to external_url, target: '_blank' do - %span.hidden-xs View on #{external_url.gsub(/\A.*?:\/\//, '')} - = icon('external-link', right: true) +.js-success-icon.hidden + = ci_icon_for_status('success') diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 6f5ee5f16c5..842b6df310d 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -35,3 +35,4 @@ Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)} = succeed '.' do != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author + = mr_assign_issues_link diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index ea618263a4a..608fdf1c5f5 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -12,6 +12,7 @@ merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + ci_environments_status_url: "#{ci_environments_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", gitlab_icon: "#{asset_path 'gitlab_logo.png'}", ci_status: "#{@merge_request.pipeline ? @merge_request.pipeline.status : ''}", ci_message: { @@ -33,4 +34,4 @@ merge_request_widget.clearEventListeners(); } - merge_request_widget = new MergeRequestWidget(opts); + merge_request_widget = new window.gl.MergeRequestWidget(opts); diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index b2ece44d966..29df1bab04e 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -8,7 +8,7 @@ .project-network .controls = form_tag namespace_project_network_path(@project.namespace, @project, @id), method: :get, class: 'form-inline network-form' do |f| - = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Input an extended SHA1 syntax", class: 'search-input form-control input-mx-250 search-sha' + = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Git revision", class: 'search-input form-control input-mx-250 search-sha' = button_tag class: 'btn btn-success' do = icon('search') .inline.prepend-left-20 diff --git a/app/views/shared/icons/_illustration_no_commits.svg b/app/views/shared/icons/_illustration_no_commits.svg new file mode 100644 index 00000000000..4f9d9add60d --- /dev/null +++ b/app/views/shared/icons/_illustration_no_commits.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 107" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="#eee" fill-rule="evenodd"><path d="m4.01 2h1.102c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-1.102c-2.218 0-4.01 1.788-4.01 4 0 .552.448 1 1 1 .552 0 1-.448 1-1 0-1.108.892-2 2.01-2m12.702 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m8.088 0c.822 0 1.554.503 1.86 1.254.208.512.791.758 1.303.55.512-.208.758-.791.55-1.303-.609-1.497-2.069-2.5-3.712-2.5h-2.188c-.552 0-1 .448-1 1 0 .552.448 1 1 1h2.188m2.01 12.518c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 6.282c0 1.108-.892 2-2.01 2h-.72c-.552 0-1 .448-1 1 0 .552.448 1 1 1h.72c2.218 0 4.01-1.788 4.01-4v-.382c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.382m-14.325 2c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-8.47 0c-.755 0-1.438-.424-1.782-1.085-.255-.49-.859-.681-1.349-.426-.49.255-.681.859-.426 1.349.684 1.316 2.046 2.162 3.556 2.162h2.57c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-2.57m-2.01-12.136c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-6.664c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.764c0 .552.448 1 1 1 .552 0 1-.448 1-1v-.764" id="0"/><circle cx="21" cy="24" r="10"/><rect width="33" height="3" x="37" y="18" rx="1.5" id="1"/><rect width="53" height="3" x="37" y="27" rx="1.5" id="2"/><path d="m131 29c0 .552.447.999.996.999h22.01c.545 0 .996-.451.996-.999v-9c0-.552-.447-.999-.996-.999h-22.01c-.545 0-.996.451-.996.999v9m.996-12h22.01c1.655 0 2.996 1.344 2.996 2.999v9c0 1.657-1.35 2.999-2.996 2.999h-22.01c-1.655 0-2.996-1.344-2.996-2.999v-9c0-1.657 1.35-2.999 2.996-2.999" id="3"/><g transform="translate(0 59)"><use xlink:href="#0"/><circle cx="21" cy="24" r="10"/><use xlink:href="#1"/><use xlink:href="#2"/><use xlink:href="#3"/></g></g></svg>
\ No newline at end of file diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index c3f4e10c954..a75653d842f 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -85,20 +85,20 @@ .issuable-form-select-holder - 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: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", + = 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 } }) .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" + = 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" .form-group - has_labels = issuable.project.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' } + = 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" - if has_due_date .col-lg-6 .form-group diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 6d307611640..22b5a6aa11b 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -8,6 +8,7 @@ - classes = local_assigns.fetch(:classes, []) - selected = local_assigns.fetch(:selected, nil) - selected_toggle = local_assigns.fetch(:selected_toggle, nil) +- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label") - dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"} - dropdown_data.merge!(data_options) - classes << 'js-extra-options' if extra_options @@ -23,7 +24,7 @@ = multi_label_name(selected, "Labels") = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable - = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label", show_footer: show_footer, show_create: show_create } + = render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create } - if show_create && project && can?(current_user, :admin_label, project) = render partial: "shared/issuable/label_page_create" = dropdown_loading diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index ab3cc33d18f..f27a9002ec2 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -2,9 +2,10 @@ - extra_class = extra_class || '' - show_menu_above = show_menu_above || false - selected_text = selected.try(:title) +- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone") - if selected.present? = hidden_field_tag(name, name == :milestone_title ? selected.title : selected.id) -= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: "Filter by milestone", toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", += dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do - if project %ul.dropdown-footer-list diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index cd89155c616..27d7a6c5bb6 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -9,6 +9,7 @@ .file-actions = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" + = link_to 'Download', download_snippet_path(@snippet), class: "btn btn-sm" = render 'shared/snippets/blob' = render 'award_emoji/awards_block', awardable: @snippet, inline: true
\ No newline at end of file diff --git a/app/workers/process_pipeline_worker.rb b/app/workers/pipeline_process_worker.rb index 26ea5f1c24d..f44227d7086 100644 --- a/app/workers/process_pipeline_worker.rb +++ b/app/workers/pipeline_process_worker.rb @@ -1,4 +1,4 @@ -class ProcessPipelineWorker +class PipelineProcessWorker include Sidekiq::Worker sidekiq_options queue: :default diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb new file mode 100644 index 00000000000..5dd443fea59 --- /dev/null +++ b/app/workers/pipeline_success_worker.rb @@ -0,0 +1,12 @@ +class PipelineSuccessWorker + include Sidekiq::Worker + sidekiq_options queue: :default + + def perform(pipeline_id) + Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| + MergeRequests::MergeWhenBuildSucceedsService + .new(pipeline.project, nil) + .trigger(pipeline) + end + end +end diff --git a/app/workers/update_pipeline_worker.rb b/app/workers/pipeline_update_worker.rb index 6ef5678073e..44a7f24e401 100644 --- a/app/workers/update_pipeline_worker.rb +++ b/app/workers/pipeline_update_worker.rb @@ -1,4 +1,4 @@ -class UpdatePipelineWorker +class PipelineUpdateWorker include Sidekiq::Worker sidekiq_options queue: :default diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb new file mode 100644 index 00000000000..df4c4a6628b --- /dev/null +++ b/app/workers/trending_projects_worker.rb @@ -0,0 +1,11 @@ +class TrendingProjectsWorker + include Sidekiq::Worker + + sidekiq_options queue: :trending_projects + + def perform + Rails.logger.info('Refreshing trending projects') + + TrendingProject.refresh! + end +end diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb new file mode 100644 index 00000000000..03f0528cdae --- /dev/null +++ b/app/workers/update_merge_requests_worker.rb @@ -0,0 +1,16 @@ +class UpdateMergeRequestsWorker + include Sidekiq::Worker + + def perform(project_id, user_id, oldrev, newrev, ref) + project = Project.find_by(id: project_id) + return unless project + + user = User.find_by(id: user_id) + return unless user + + MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref) + + push_data = Gitlab::DataBuilder::Push.build(project, user, oldrev, newrev, ref, []) + SystemHooksService.new.execute_hooks(push_data, :push_hooks) + end +end |