diff options
Diffstat (limited to 'app')
89 files changed, 1199 insertions, 330 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 6df2ecf57a2..1cd2302111e 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -16,9 +16,6 @@ .replace(':id', group_id); return $.ajax({ url: url, - data: { - private_token: gon.api_token - }, dataType: "json" }).done(function(group) { return callback(group); @@ -31,7 +28,6 @@ return $.ajax({ url: url, data: { - private_token: gon.api_token, search: query, per_page: 20 }, @@ -46,7 +42,6 @@ return $.ajax({ url: url, data: { - private_token: gon.api_token, search: query, per_page: 20 }, @@ -61,7 +56,6 @@ return $.ajax({ url: url, data: { - private_token: gon.api_token, search: query, order_by: order, per_page: 20 @@ -74,7 +68,6 @@ newLabel: function(project_id, data, callback) { var url = Api.buildUrl(Api.labelsPath) .replace(':id', project_id); - data.private_token = gon.api_token; return $.ajax({ url: url, type: "POST", @@ -93,7 +86,6 @@ return $.ajax({ url: url, data: { - private_token: gon.api_token, search: query, per_page: 20 }, diff --git a/app/assets/javascripts/cycle-analytics.js.es6 b/app/assets/javascripts/cycle-analytics.js.es6 new file mode 100644 index 00000000000..cd9886ba58d --- /dev/null +++ b/app/assets/javascripts/cycle-analytics.js.es6 @@ -0,0 +1,93 @@ +((global) => { + + const COOKIE_NAME = 'cycle_analytics_help_dismissed'; + const store = gl.cycleAnalyticsStore = { + isLoading: true, + hasError: false, + isHelpDismissed: $.cookie(COOKIE_NAME), + analytics: {} + }; + + gl.CycleAnalytics = class CycleAnalytics { + constructor() { + const that = this; + + this.vue = new Vue({ + el: '#cycle-analytics', + name: 'CycleAnalytics', + created: this.fetchData(), + data: store, + methods: { + dismissLanding() { + that.dismissLanding(); + } + } + }); + } + + fetchData(options) { + store.isLoading = true; + options = options || { startDate: 30 }; + + $.ajax({ + url: $('#cycle-analytics').data('request-path'), + method: 'GET', + dataType: 'json', + contentType: 'application/json', + data: { start_date: options.startDate } + }).done((data) => { + this.decorateData(data); + this.initDropdown(); + }) + .error((data) => { + this.handleError(data); + }) + .always(() => { + store.isLoading = false; + }) + } + + decorateData(data) { + data.summary = data.summary || []; + data.stats = data.stats || []; + + data.summary.forEach((item) => { + item.value = item.value || '-'; + }); + + data.stats.forEach((item) => { + item.value = item.value || '- - -'; + }); + + store.analytics = data; + } + + handleError(data) { + store.hasError = true; + new Flash('There was an error while fetching cycle analytics data.', 'alert'); + } + + dismissLanding() { + store.isHelpDismissed = true; + $.cookie(COOKIE_NAME, true, { + path: gon.relative_url_root || '/' + }); + } + + initDropdown() { + const $dropdown = $('.js-ca-dropdown'); + const $label = $dropdown.find('.dropdown-label'); + + $dropdown.find('li a').off('click').on('click', (e) => { + e.preventDefault(); + const $target = $(e.currentTarget); + const value = $target.data('value'); + + $label.text($target.text().trim()); + this.fetchData({ startDate: value }); + }) + } + + } + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 99b16f7d59b..ddf11ecf34c 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -94,6 +94,11 @@ break; case "projects:merge_requests:conflicts": window.mcui = new MergeConflictResolver() + break; + case 'projects:merge_requests:index': + shortcut_handler = new ShortcutsNavigation(); + Issuable.init(); + break; case 'dashboard:activity': new Activities(); break; @@ -185,6 +190,9 @@ new gl.ProtectedBranchCreate(); new gl.ProtectedBranchEditList(); break; + case 'projects:cycle_analytics:show': + new gl.CycleAnalytics(); + break; } switch (path.first()) { case 'admin': diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index c05cda25bbd..1b6db641200 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -608,27 +608,28 @@ } } field = []; - fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName(selectedObject) : this.options.fieldName; value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id; if (isInput) { field = $(this.el); } else if(value) { field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']"); } - if (field.length && el.hasClass(ACTIVE_CLASS)) { + if (el.hasClass(ACTIVE_CLASS)) { el.removeClass(ACTIVE_CLASS); - if (isInput) { - field.val(''); - } else { - field.remove(); + if (field && field.length) { + if (isInput) { + field.val(''); + } else { + field.remove(); + } } } else if (el.hasClass(INDETERMINATE_CLASS)) { el.addClass(ACTIVE_CLASS); el.removeClass(INDETERMINATE_CLASS); - if (field.length && value == null) { + if (field && field.length && value == null) { field.remove(); } - if (!field.length && fieldName) { + if ((!field || !field.length) && fieldName) { this.addInput(fieldName, value, selectedObject); } } else { @@ -638,15 +639,15 @@ this.dropdown.parent().find("input[name='" + fieldName + "']").remove(); } } - if (field.length && value == null) { + if (field && field.length && value == null) { field.remove(); } // Toggle active class for the tick mark el.addClass(ACTIVE_CLASS); if (value != null) { - if (!field.length && fieldName) { + if ((!field || !field.length) && fieldName) { this.addInput(fieldName, value, selectedObject); - } else if (field.length) { + } else if (field && field.length) { field.val(value).trigger('change'); } } @@ -796,4 +797,4 @@ }); }; -}).call(this);
\ No newline at end of file +}).call(this); diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 53faaa38a0c..73e2664e9c0 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -15,25 +15,32 @@ return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>'); }, initSearch: function() { - this.timer = null; - return $('#issue_search').off('keyup').on('keyup', function() { - clearTimeout(this.timer); - return this.timer = setTimeout(function() { - var $form, $input, $search; - $search = $('#issue_search'); - $form = $('.js-filter-form'); - $input = $("input[name='" + ($search.attr('name')) + "']", $form); - if ($input.length === 0) { - $form.append("<input type='hidden' name='" + ($search.attr('name')) + "' value='" + (_.escape($search.val())) + "'/>"); - } else { - $input.val($search.val()); - } - if ($search.val() !== '') { - return Issuable.filterResults($form); - } - }, 500); + // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing + const debouncedExecSearch = _.debounce(Issuable.executeSearch, 500, false); + + $('#issuable_search').off('keyup').on('keyup', debouncedExecSearch); + + // ensures existing filters are preserved when manually submitted + $('#issue_search_form').on('submit', (e) => { + e.preventDefault(); + debouncedExecSearch(e); }); }, + executeSearch: function(e) { + const $search = $('#issuable_search'); + const $searchName = $search.attr('name'); + const $searchValue = $search.val(); + const $filtersForm = $('.js-filter-form'); + const $input = $(`input[name='${$searchName}']`, $filtersForm); + + if (!$input.length) { + $filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`); + } else { + $input.val($searchValue); + } + + Issuable.filterResults($filtersForm); + }, initLabelFilterRemove: function() { return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) { var $button; diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 5bf900f3e1d..8e38ccf7e44 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -7,7 +7,6 @@ function ProjectFindFile(element1, options) { this.element = element1; this.options = options; - this.goToBlob = bind(this.goToBlob, this); this.goToTree = bind(this.goToTree, this); this.selectRowDown = bind(this.selectRowDown, this); this.selectRowUp = bind(this.selectRowUp, this); @@ -36,16 +35,6 @@ } }; })(this)); - return this.element.find(".tree-content-holder .tree-table").on("click", function(event) { - var path; - if (event.target.nodeName !== "A") { - path = this.element.find(".tree-item-file-name a", this).attr("href"); - if (path) { - return location.href = path; - } - } - }); - // init event }; ProjectFindFile.prototype.findFile = function() { @@ -121,11 +110,12 @@ // make tbody row html ProjectFindFile.prototype.makeHtml = function(filePath, matches, blobItemUrl) { var $tr; - $tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>"); + $tr = $("<tr class='tree-item'><td class='tree-item-file-name link-container'><a><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'></span></a></td></tr>"); if (matches) { $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl)); } else { - $tr.find("a").attr("href", blobItemUrl).text(filePath); + $tr.find("a").attr("href", blobItemUrl); + $tr.find(".str-truncated").text(filePath); } return $tr; }; @@ -164,14 +154,6 @@ return location.href = this.options.treeUrl; }; - ProjectFindFile.prototype.goToBlob = function() { - var path; - path = this.element.find(".tree-item.selected .tree-item-file-name a").attr("href"); - if (path) { - return location.href = path; - } - }; - return ProjectFindFile; })(); diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branch_edit.js.es6 index 40bc4adb71b..15a6dca2875 100644 --- a/app/assets/javascripts/protected_branch_edit.js.es6 +++ b/app/assets/javascripts/protected_branch_edit.js.es6 @@ -40,7 +40,6 @@ dataType: 'json', data: { _method: 'PATCH', - id: this.$wrap.data('banchId'), protected_branch: { merge_access_levels_attributes: [{ id: this.$allowedToMergeDropdown.data('access-level-id'), diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index f5223207f3a..2432ddb72f4 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -129,8 +129,6 @@ position: relative; .avatar-holder { - margin-bottom: 16px; - .avatar, .identicon { margin: 0 auto; float: none; @@ -143,13 +141,7 @@ .cover-title { color: $gl-header-color; - margin: 0; - font-size: 24px; - font-weight: normal; - margin-bottom: 10px; - color: #4c4e54; font-size: 23px; - line-height: 1.1; h1 { color: $gl-gray-dark; @@ -213,6 +205,9 @@ } } } + &.user-cover-block { + padding: 24px 0 0; + } .group-info { diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index 0c21d0240b3..7ae309ba103 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -3,7 +3,6 @@ margin: 0; margin-bottom: $gl-padding; font-size: 14px; - z-index: 100; .flash-notice { @extend .alert; @@ -41,4 +40,3 @@ } } } - diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 553768b2e68..ea43f4afc37 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -99,8 +99,7 @@ .top-area { @include clearfix; - - border-bottom: 1px solid #eee; + border-bottom: 1px solid $btn-gray-hover; .nav-text { padding-top: 16px; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 2582cde5a71..9f2d53d5206 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -204,7 +204,7 @@ body { } h1, h2, h3, h4, h5, h6 { - color: $gl-header-color; + color: $gl-title-color; font-weight: 600; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 9f563a4de35..14ec310de2d 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -102,7 +102,7 @@ $gl-grayish-blue: #7f8fa4; $gl-gray: $gl-text-color; $gl-gray-dark: #313236; $gl-gray-light: $gl-placeholder-color; -$gl-header-color: $gl-title-color; +$gl-header-color: #4c4e54; /* * Lists @@ -270,6 +270,12 @@ $calendar-border-color: rgba(#000, .1); $calendar-unselectable-bg: $gray-light; /* + * Cycle Analytics + */ +$cycle-analytics-box-padding: 30px; +$cycle-analytics-box-text-color: #8c8c8c; + +/* * Personal Access Tokens */ $personal-access-tokens-disabled-label-color: #bbb; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index c879074c7fe..a5a260d4c8f 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -109,6 +109,10 @@ width: 100%; } + .block-first { + padding: 5px 16px 11px; + } + .js-build-variable { color: $code-color; } diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss new file mode 100644 index 00000000000..778471a34d7 --- /dev/null +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -0,0 +1,144 @@ +#cycle-analytics { + margin: 24px auto 0; + max-width: 800px; + position: relative; + + .panel { + + .content-block { + padding: 24px 0; + border-bottom: none; + position: relative; + + @media (max-width: $screen-sm-min) { + padding: 6px 0 24px; + } + } + + .column { + text-align: center; + + @media (max-width: $screen-sm-min) { + padding: 15px 0; + } + + .header { + font-size: 30px; + line-height: 38px; + font-weight: normal; + margin: 0; + } + + .text { + color: $layout-link-gray; + margin: 0; + } + + &:last-child { + text-align: right; + + @media (max-width: $screen-sm-min) { + text-align: center; + } + } + } + + .dropdown { + top: 13px; + } + } + + .bordered-box { + border: 1px solid $border-color; + @include border-radius($border-radius-default); + + } + + .content-list { + li { + padding: 18px $gl-padding $gl-padding; + + .container-fluid { + padding: 0; + } + } + + .title-col { + p { + margin: 0; + + &.title { + line-height: 19px; + font-size: 15px; + font-weight: 600; + color: $gl-title-color; + } + + &.text { + color: $layout-link-gray; + + &.value-col { + color: $gl-title-color; + } + } + } + } + + .value-col { + text-align: right; + + span { + position: relative; + vertical-align: middle; + top: 3px; + } + } + } + + .landing { + margin-bottom: $gl-padding; + overflow: hidden; + + .dismiss-icon { + position: absolute; + right: $cycle-analytics-box-padding; + cursor: pointer; + color: #b2b2b2; + } + + .svg-container { + text-align: center; + + svg { + width: 136px; + height: 136px; + } + } + + .inner-content { + @media (max-width: $screen-sm-min) { + padding: 0 28px; + text-align: center; + } + + h4 { + color: $gl-text-color; + font-size: 17px; + } + + p { + color: $cycle-analytics-box-text-color; + margin-bottom: $gl-padding; + } + } + } + + .fa-spinner { + font-size: 28px; + position: relative; + margin-left: -20px; + left: 50%; + margin-top: 36px; + } + +} diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index b657ca47d38..732dc645c66 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -55,3 +55,16 @@ } } } + +.groups-header { + + @media (min-width: $screen-sm-min) { + .nav-links { + width: 35%; + } + + .nav-controls { + width: 65%; + } + } +} diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index b94f524b513..6b865730487 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -2,13 +2,17 @@ max-width: 90%; } -li.milestone { - h4 { - font-weight: bold; - } +.milestones { + .milestone { + padding: 10px 16px; + + h4 { + font-weight: bold; + } - .progress { - height: 6px; + .progress { + height: 6px; + } } } @@ -64,3 +68,14 @@ li.milestone { border-bottom: 1px solid $border-color; padding: 20px 0; } + +@media (max-width: $screen-sm-min) { + .milestone-actions { + @include clearfix(); + padding-top: $gl-vert-padding; + + .btn:first-child { + margin-left: 0; + } + } +} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 1b4d12d3053..b035bfc9f3c 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -177,6 +177,10 @@ border-bottom: 2px solid $border-color; } } + + a { + display: block; + } } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 6f58203f49c..0fcdaf94a21 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -93,8 +93,9 @@ .profile-user-bio { // Limits the width of the user bio for readability. - max-width: 750px; - margin: auto; + max-width: 600px; + margin: 15px auto 0; + padding: 0 16px; } .user-avatar-button { @@ -212,6 +213,28 @@ } .user-profile { + .cover-controls a { + margin-left: 5px; + } + .profile-header { + margin: 0 auto; + .avatar-holder { + width: 90px; + display: inline-block; + } + .user-info { + display: inline-block; + text-align: left; + vertical-align: middle; + margin-left: 15px; + .handle { + color: $gl-gray-light; + } + .member-date { + margin-bottom: 4px; + } + } + } @media (max-width: $screen-xs-max) { .cover-block { padding-top: 20px; @@ -219,16 +242,26 @@ .cover-controls { position: static; + padding: 0 16px; margin-bottom: 20px; + display: -webkit-flex; + display: flex; .btn { - display: inline-block; - width: 46%; + -webkit-flex-grow: 1; + flex-grow: 1; + &:first-child { + margin-left: 0; + } } } } } +.user-profile-nav { + margin-top: 15px; +} + table.u2f-registrations { th:not(:last-child), td:not(:last-child) { border-right: solid 1px transparent; diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index 5270aea4e79..4d5df566d9b 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -12,11 +12,18 @@ .snippet-file-content { border-radius: 3px; + margin-bottom: $gl-padding; + .btn-clipboard { @extend .btn; } } +.project-snippets .awards { + border-bottom: 1px solid $table-border-color; + padding-bottom: $gl-padding; +} + .snippet-title { font-size: 24px; font-weight: 600; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 1778c069706..7b6577c513e 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -55,6 +55,15 @@ } .tree-item { + .link-container { + padding: 0; + + a { + padding: 10px $gl-padding; + display: block; + } + } + .tree-item-file-name { max-width: 320px; vertical-align: middle; diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb index 172d5344b7a..3717c49f272 100644 --- a/app/controllers/concerns/toggle_award_emoji.rb +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -10,7 +10,9 @@ module ToggleAwardEmoji if awardable.user_can_award?(current_user, name) awardable.toggle_award_emoji(name, current_user) - TodoService.new.new_award_emoji(to_todoable(awardable), current_user) + + todoable = to_todoable(awardable) + TodoService.new.new_award_emoji(todoable, current_user) if todoable render json: { ok: true } else @@ -24,8 +26,10 @@ module ToggleAwardEmoji case awardable when Note awardable.noteable - else + when MergeRequest, Issue awardable + when Snippet + nil end end diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 06d96774754..34d5d99558e 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -11,10 +11,8 @@ class JwtController < ApplicationController service = SERVICES[params[:service]] return head :not_found unless service - @authentication_result ||= Gitlab::Auth::Result.new - result = service.new(@authentication_result.project, @authentication_result.actor, auth_params). - execute(authentication_abilities: @authentication_result.authentication_abilities) + execute(authentication_abilities: @authentication_result.authentication_abilities || []) render json: result, status: result[:http_status] end @@ -22,6 +20,8 @@ class JwtController < ApplicationController private def authenticate_project_or_user + @authentication_result = Gitlab::Auth::Result.new + authenticate_with_http_basic do |login, password| @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip) diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb new file mode 100644 index 00000000000..16a7b1fc6e2 --- /dev/null +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -0,0 +1,67 @@ +class Projects::CycleAnalyticsController < Projects::ApplicationController + include ActionView::Helpers::DateHelper + include ActionView::Helpers::TextHelper + + before_action :authorize_read_cycle_analytics! + + def show + @cycle_analytics = CycleAnalytics.new(@project, from: parse_start_date) + + respond_to do |format| + format.html + format.json { render json: cycle_analytics_json } + end + end + + private + + def parse_start_date + case cycle_analytics_params[:start_date] + when '30' then 30.days.ago + when '90' then 90.days.ago + else 90.days.ago + end + end + + def cycle_analytics_params + return {} unless params[:cycle_analytics].present? + + { start_date: params[:cycle_analytics][:start_date] } + end + + def cycle_analytics_json + cycle_analytics_view_data = [[:issue, "Issue", "Time before an issue gets scheduled"], + [:plan, "Plan", "Time before an issue starts implementation"], + [:code, "Code", "Time until first merge request"], + [:test, "Test", "Total test time for all commits/merges"], + [:review, "Review", "Time between merge request creation and merge/close"], + [:staging, "Staging", "From merge request merge until deploy to production"], + [:production, "Production", "From issue creation until deploy to production"]] + + stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)| + value = @cycle_analytics.send(stage_method).presence + + stats << { + title: stage_text, + description: stage_description, + value: value && !value.zero? ? distance_of_time_in_words(value) : nil + } + stats + end + + issues = @cycle_analytics.summary.new_issues + commits = @cycle_analytics.summary.commits + deploys = @cycle_analytics.summary.deploys + + summary = [ + { title: "New Issue".pluralize(issues), value: issues }, + { title: "Commit".pluralize(commits), value: commits }, + { title: "Deploy".pluralize(deploys), value: deploys } + ] + + { + summary: summary, + stats: stats + } + end +end diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index cbfd3cab3dd..383e184d796 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -32,11 +32,11 @@ class Projects::GitHttpClientController < Projects::ApplicationController return # Allow access end elsif allow_kerberos_spnego_auth? && spnego_provided? - user = find_kerberos_user + kerberos_user = find_kerberos_user - if user + if kerberos_user @authentication_result = Gitlab::Auth::Result.new( - user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities) + kerberos_user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities) send_final_spnego_response return # Allow access diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index de02e28e384..3eb13a121bf 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -23,18 +23,9 @@ class Projects::IssuesController < Projects::ApplicationController respond_to :html def index - terms = params['issue_search'] @issues = issues_collection - - if terms.present? - if terms =~ /\A#(\d+)\z/ - @issues = @issues.where(iid: $1) - else - @issues = @issues.full_search(terms) - end - end - @issues = @issues.page(params[:page]) + @labels = @project.labels.where(title: params[:label_name]) respond_to do |format| diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 0288ee87717..935417d4ae8 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -31,17 +31,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts] def index - terms = params['issue_search'] @merge_requests = merge_requests_collection - - if terms.present? - if terms =~ /\A[#!](\d+)\z/ - @merge_requests = @merge_requests.where(iid: $1) - else - @merge_requests = @merge_requests.full_search(terms) - end - end - @merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.preload(:target_project) diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 17ceefec3b8..e290a0eadda 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -1,6 +1,8 @@ class Projects::SnippetsController < Projects::ApplicationController + include ToggleAwardEmoji + before_action :module_enabled - before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji] # Allow read any snippet before_action :authorize_read_project_snippet!, except: [:new, :create, :index] @@ -80,6 +82,7 @@ class Projects::SnippetsController < Projects::ApplicationController def snippet @snippet ||= @project.snippets.find(params[:id]) end + alias_method :awardable, :snippet def authorize_read_project_snippet! return render_404 unless can?(current_user, :read_project_snippet, @snippet) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 61517d21f9f..d01e0dedf52 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -6,8 +6,6 @@ class SearchController < ApplicationController layout 'search' def show - return if params[:search].nil? || params[:search].blank? - if params[:project_id].present? @project = Project.find_by(id: params[:project_id]) @project = nil unless can?(current_user, :download_code, @project) @@ -18,6 +16,8 @@ class SearchController < ApplicationController @group = nil unless can?(current_user, :read_group, @group) end + return if params[:search].nil? || params[:search].blank? + @search_term = params[:search] @scope = params[:scope] diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 2a17c1f34db..d198782138a 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,4 +1,6 @@ class SnippetsController < ApplicationController + include ToggleAwardEmoji + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] # Allow read snippet @@ -85,6 +87,7 @@ class SnippetsController < ApplicationController PersonalSnippet.find(params[:id]) end end + alias_method :awardable, :snippet def authorize_read_snippet! authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 60996b181f2..8f9ef8f725c 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -216,7 +216,14 @@ class IssuableFinder end def by_search(items) - items = items.search(search) if search + if search + items = + if search =~ iid_pattern + items.where(iid: $~[:iid]) + else + items.full_search(search) + end + end items end diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index c2befa5a5b3..be00a219205 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -25,4 +25,8 @@ class IssuesFinder < IssuableFinder def init_collection Issue.visible_to_user(current_user) end + + def iid_pattern + @iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z} + end end diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index b258216d0d4..3b254e7d9d5 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -19,4 +19,14 @@ class MergeRequestsFinder < IssuableFinder def klass MergeRequest end + + private + + def iid_pattern + @iid_pattern ||= %r{\A[ + #{Regexp.escape(MergeRequest.reference_prefix)} + #{Regexp.escape(Issue.reference_prefix)} + ](?<iid>\d+)\z + }x + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 5f3765cad0d..1df430e6279 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -249,7 +249,7 @@ module ApplicationHelper milestone_title: params[:milestone_title], assignee_id: params[:assignee_id], author_id: params[:author_id], - issue_search: params[:issue_search], + search: params[:search], label_name: params[:label_name] } diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb new file mode 100644 index 00000000000..aa134cea31c --- /dev/null +++ b/app/helpers/award_emoji_helper.rb @@ -0,0 +1,9 @@ +module AwardEmojiHelper + def toggle_award_url(awardable) + if @project + url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) + else + url_for([:toggle_award_emoji, awardable]) + end + end +end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index a322a90cc4e..670a7ca36f4 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -46,6 +46,10 @@ module GitlabRoutingHelper namespace_project_environments_path(project.namespace, project, *args) end + def project_cycle_analytics_path(project, *args) + namespace_project_cycle_analytics_path(project.namespace, project, *args) + end + def project_builds_path(project, *args) namespace_project_builds_path(project.namespace, project, *args) end @@ -66,6 +70,10 @@ module GitlabRoutingHelper namespace_project_runner_path(@project.namespace, @project, runner, *args) end + def environment_path(environment, *args) + namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args) + end + def issue_path(entity, *args) namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args) end @@ -98,6 +106,14 @@ module GitlabRoutingHelper end end + def toggle_award_emoji_personal_snippet_path(*args) + toggle_award_emoji_snippet_path(*args) + end + + def toggle_award_emoji_namespace_project_project_snippet_path(*args) + toggle_award_emoji_namespace_project_snippet_path(*args) + end + ## Members def project_members_url(project, *args) namespace_project_project_members_url(project.namespace, project) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index b3e6e468ecd..a11c313a6b8 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -35,6 +35,30 @@ module MilestonesHelper milestone.issues.with_label(label.title).send(state).size end + # Returns count of milestones for different states + # Uses explicit hash keys as the 'opened' state URL params differs from the db value + # and we need to add the total + def milestone_counts(milestones) + counts = milestones.reorder(nil).group(:state).count + + { + opened: counts['active'] || 0, + closed: counts['closed'] || 0, + all: counts.values.sum || 0 + } + end + + # Show 'active' class if provided GET param matches check + # `or_blank` allows the function to return 'active' when given an empty param + # Could be refactored to be simpler but that may make it harder to read + def milestone_class_for_state(param, check, match_blank_param = false) + if match_blank_param + 'active' if param.blank? || param == check + else + 'active' if param == check + end + end + def milestone_progress_bar(milestone) options = { class: 'progress-bar progress-bar-success', diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index dd984aef318..522e2264bb8 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -91,7 +91,7 @@ module Ci sha: build.sha, ref: build.ref, tag: build.tag, - options: build.options[:environment], + options: build.options.to_h[:environment], variables: build.variables) service.execute(build) end @@ -493,8 +493,11 @@ module Ci end def hide_secrets(trace) - trace = Ci::MaskSecret.mask(trace, project.runners_token) if project - trace = Ci::MaskSecret.mask(trace, token) + return unless trace + + trace = trace.dup + Ci::MaskSecret.mask!(trace, project.runners_token) if project + Ci::MaskSecret.mask!(trace, token) trace end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 895eac1a258..663c5b1e231 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -56,6 +56,16 @@ module Ci pipeline.finished_at = Time.now 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) + end + + after_transition any => [:success] do |pipeline| + MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)). + update_all(latest_build_finished_at: pipeline.finished_at) + end + before_transition do |pipeline| pipeline.update_duration end @@ -280,6 +290,16 @@ module Ci project.execute_services(data, :pipeline_hooks) end + # Merge requests for which the current pipeline is running against + # the merge request's latest commit. + def merge_requests + @merge_requests ||= + begin + project.merge_requests.where(source_branch: self.ref). + select { |merge_request| merge_request.pipeline.try(:id) == self.id } + end + end + private def pipeline_data diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index d8d4575bb4d..073ac4c1b65 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -71,6 +71,12 @@ module Awardable end end + def user_authored?(current_user) + author = self.respond_to?(:author) ? self.author : self.user + + author == current_user + end + def awarded_emoji?(emoji_name, current_user) award_emoji.where(name: emoji_name, user: current_user).exists? end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 22231b2e0f0..ff465d2c745 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -28,10 +28,13 @@ module Issuable loaded? && to_a.all? { |note| note.association(:award_emoji).loaded? } end end + has_many :label_links, as: :target, dependent: :destroy has_many :labels, through: :label_links has_many :todos, as: :target, dependent: :destroy + has_one :metrics + validates :author, presence: true validates :title, presence: true, length: { within: 0..255 } @@ -81,6 +84,7 @@ module Issuable acts_as_paranoid after_save :update_assignee_cache_counts, if: :assignee_id_changed? + after_save :record_metrics def update_assignee_cache_counts # make sure we flush the cache for both the old *and* new assignee @@ -196,10 +200,6 @@ module Issuable end end - def user_authored?(user) - user == author - end - def subscribed_without_subscriptions?(user) participants(user).include?(user) end @@ -286,4 +286,9 @@ module Issuable def can_move?(*) false end + + def record_metrics + metrics = self.metrics || create_metrics + metrics.record! + end end diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb new file mode 100644 index 00000000000..be295487fd2 --- /dev/null +++ b/app/models/cycle_analytics.rb @@ -0,0 +1,97 @@ +class CycleAnalytics + include Gitlab::Database::Median + include Gitlab::Database::DateTime + + def initialize(project, from:) + @project = project + @from = from + end + + def summary + @summary ||= Summary.new(@project, from: @from) + end + + def issue + calculate_metric(:issue, + Issue.arel_table[:created_at], + [Issue::Metrics.arel_table[:first_associated_with_milestone_at], + Issue::Metrics.arel_table[:first_added_to_board_at]]) + end + + def plan + calculate_metric(:plan, + [Issue::Metrics.arel_table[:first_associated_with_milestone_at], + Issue::Metrics.arel_table[:first_added_to_board_at]], + Issue::Metrics.arel_table[:first_mentioned_in_commit_at]) + end + + def code + calculate_metric(:code, + Issue::Metrics.arel_table[:first_mentioned_in_commit_at], + MergeRequest.arel_table[:created_at]) + end + + def test + calculate_metric(:test, + MergeRequest::Metrics.arel_table[:latest_build_started_at], + MergeRequest::Metrics.arel_table[:latest_build_finished_at]) + end + + def review + calculate_metric(:review, + MergeRequest.arel_table[:created_at], + MergeRequest::Metrics.arel_table[:merged_at]) + end + + def staging + calculate_metric(:staging, + MergeRequest::Metrics.arel_table[:merged_at], + MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) + end + + def production + calculate_metric(:production, + Issue.arel_table[:created_at], + MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) + end + + private + + def calculate_metric(name, start_time_attrs, end_time_attrs) + cte_table = Arel::Table.new("cte_table_for_#{name}") + + # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time). + # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time). + # We compute the (end_time - start_time) interval, and give it an alias based on the current + # cycle analytics stage. + interval_query = Arel::Nodes::As.new( + cte_table, + subtract_datetimes(base_query, end_time_attrs, start_time_attrs, name.to_s)) + + median_datetime(cte_table, interval_query, name) + end + + # Join table with a row for every <issue,merge_request> pair (where the merge request + # 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 + arel_table = MergeRequestsClosingIssues.arel_table + + # Load issues + query = arel_table.join(Issue.arel_table).on(Issue.arel_table[:id].eq(arel_table[:issue_id])). + join(Issue::Metrics.arel_table).on(Issue.arel_table[:id].eq(Issue::Metrics.arel_table[:issue_id])). + where(Issue.arel_table[:project_id].eq(@project.id)). + where(Issue.arel_table[:deleted_at].eq(nil)). + where(Issue.arel_table[:created_at].gteq(@from)) + + # Load merge_requests + query = query.join(MergeRequest.arel_table, Arel::Nodes::OuterJoin). + on(MergeRequest.arel_table[:id].eq(arel_table[:merge_request_id])). + 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)) + end +end diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb new file mode 100644 index 00000000000..53b2cacb131 --- /dev/null +++ b/app/models/cycle_analytics/summary.rb @@ -0,0 +1,24 @@ +class CycleAnalytics + class Summary + def initialize(project, from:) + @project = project + @from = from + end + + def new_issues + @project.issues.created_after(@from).count + end + + def commits + repository = @project.repository.raw_repository + + if @project.default_branch + repository.log(ref: @project.default_branch, after: @from).count + end + end + + def deploys + @project.deployments.where("created_at > ?", @from).count + end + end +end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 1e338889714..07d7e19e70d 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -42,4 +42,38 @@ class Deployment < ActiveRecord::Base project.repository.is_ancestor?(commit.id, sha) end + + def update_merge_request_metrics! + return unless environment.update_merge_request_metrics? + + merge_requests = project.merge_requests. + joins(:metrics). + where(target_branch: self.ref, merge_request_metrics: { first_deployed_to_production_at: nil }). + where("merge_request_metrics.merged_at <= ?", self.created_at) + + if previous_deployment + merge_requests = merge_requests.where("merge_request_metrics.merged_at >= ?", previous_deployment.created_at) + end + + # Need to use `map` instead of `select` because MySQL doesn't allow `SELECT`ing from the same table + # that we're updating. + merge_request_ids = + if Gitlab::Database.postgresql? + merge_requests.select(:id) + elsif Gitlab::Database.mysql? + merge_requests.map(&:id) + end + + MergeRequest::Metrics. + where(merge_request_id: merge_request_ids, first_deployed_to_production_at: nil). + update_all(first_deployed_to_production_at: self.created_at) + end + + def previous_deployment + @previous_deployment ||= + project.deployments.joins(:environment). + where(environments: { name: self.environment.name }, ref: self.ref). + where.not(id: self.id). + take + end end diff --git a/app/models/environment.rb b/app/models/environment.rb index 33c9abf382a..49e0a20640c 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -43,4 +43,8 @@ class Environment < ActiveRecord::Base last_deployment.includes_commit?(commit) end + + def update_merge_request_metrics? + self.name == "production" + end end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index da7c265a371..bda2b5c5d5d 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -8,7 +8,8 @@ class GlobalMilestone milestones = milestones.group_by(&:title) milestones.map do |title, milestones| - new(title, milestones) + milestones_relation = Milestone.where(id: milestones.map(&:id)) + new(title, milestones_relation) end end @@ -31,7 +32,7 @@ class GlobalMilestone end def projects - @projects ||= Project.for_milestones(milestones.map(&:id)) + @projects ||= Project.for_milestones(milestones.select(:id)) end def state @@ -53,19 +54,19 @@ class GlobalMilestone end def issues - @issues ||= Issue.of_milestones(milestones.map(&:id)).includes(:project) + @issues ||= Issue.of_milestones(milestones.select(:id)).includes(:project, :assignee, :labels) end def merge_requests - @merge_requests ||= MergeRequest.of_milestones(milestones.map(&:id)).includes(:target_project) + @merge_requests ||= MergeRequest.of_milestones(milestones.select(:id)).includes(:target_project, :assignee, :labels) end def participants - @participants ||= milestones.map(&:participants).flatten.compact.uniq + @participants ||= milestones.includes(:participants).map(&:participants).flatten.compact.uniq end def labels - @labels ||= GlobalLabel.build_collection(milestones.map(&:labels).flatten) + @labels ||= GlobalLabel.build_collection(milestones.includes(:labels).map(&:labels).flatten) .sort_by!(&:title) end diff --git a/app/models/issue.rb b/app/models/issue.rb index 788611305fe..abd58e0454a 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -23,6 +23,8 @@ class Issue < ActiveRecord::Base has_many :events, as: :target, dependent: :destroy + has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all + validates :project, presence: true scope :cared, ->(user) { where(assignee_id: user) } @@ -36,6 +38,8 @@ class Issue < ActiveRecord::Base scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') } scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') } + scope :created_after, -> (datetime) { where("created_at >= ?", datetime) } + attr_spammable :title, spam_title: true attr_spammable :description, spam_description: true diff --git a/app/models/issue/metrics.rb b/app/models/issue/metrics.rb new file mode 100644 index 00000000000..012d545c440 --- /dev/null +++ b/app/models/issue/metrics.rb @@ -0,0 +1,21 @@ +class Issue::Metrics < ActiveRecord::Base + belongs_to :issue + + def record! + if issue.milestone_id.present? && self.first_associated_with_milestone_at.blank? + self.first_associated_with_milestone_at = Time.now + end + + if issue_assigned_to_list_label? && self.first_added_to_board_at.blank? + self.first_added_to_board_at = Time.now + end + + self.save + end + + private + + def issue_assigned_to_list_label? + issue.labels.any? { |label| label.lists.present? } + end +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 75f48fd4ba5..2dcf7f89bfc 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -16,6 +16,8 @@ class MergeRequest < ActiveRecord::Base has_many :events, as: :target, dependent: :destroy + has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all + serialize :merge_params, Hash after_create :ensure_merge_request_diff, unless: :importing? @@ -501,6 +503,19 @@ class MergeRequest < ActiveRecord::Base target_project end + # If the merge request closes any issues, save this information in the + # `MergeRequestsClosingIssues` model. This is a performance optimization. + # Calculating this information for a number of merge requests requires + # running `ReferenceExtractor` on each of them separately. + def cache_merge_request_closes_issues!(current_user = self.author) + transaction do + self.merge_requests_closing_issues.delete_all + closes_issues(current_user).each do |issue| + self.merge_requests_closing_issues.create!(issue: issue) + end + end + end + def closes_issue?(issue) closes_issues.include?(issue) end @@ -508,7 +523,8 @@ class MergeRequest < ActiveRecord::Base # Return the set of issues that will be closed if this merge request is accepted. def closes_issues(current_user = self.author) if target_branch == project.default_branch - messages = commits.map(&:safe_message) << description + messages = [description] + messages.concat(commits.map(&:safe_message)) if merge_request_diff Gitlab::ClosingIssueExtractor.new(project, current_user). closed_by_message(messages.join("\n")) @@ -654,9 +670,12 @@ class MergeRequest < ActiveRecord::Base def environments return [] unless diff_head_commit - target_project.environments.select do |environment| - environment.includes_commit?(diff_head_commit) - end + 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 def state_human_name @@ -745,10 +764,23 @@ class MergeRequest < ActiveRecord::Base end def all_pipelines - @all_pipelines ||= - if diff_head_sha && source_project - source_project.pipelines.order(id: :desc).where(sha: commits_sha, ref: source_branch) - end + return unless source_project + + @all_pipelines ||= begin + sha = if persisted? + all_commits_sha + else + diff_head_sha + end + + source_project.pipelines.order(id: :desc). + where(sha: sha, ref: source_branch) + end + end + + # Note that this could also return SHA from now dangling commits + def all_commits_sha + merge_request_diffs.flat_map(&:commits_sha).uniq end def merge_commit diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb new file mode 100644 index 00000000000..99c49a020c9 --- /dev/null +++ b/app/models/merge_request/metrics.rb @@ -0,0 +1,11 @@ +class MergeRequest::Metrics < ActiveRecord::Base + belongs_to :merge_request + + def record! + if merge_request.merged? && self.merged_at.blank? + self.merged_at = Time.now + end + + self.save + end +end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 18c583add88..36b8b70870b 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -30,6 +30,10 @@ class MergeRequestDiff < ActiveRecord::Base select(column_names - ['st_diffs']) end + def st_commits + super || [] + end + # Collect information about commits and diff from repository # and save it to the database as serialized data def save_git_content @@ -83,7 +87,7 @@ class MergeRequestDiff < ActiveRecord::Base end def commits - @commits ||= load_commits(st_commits || []) + @commits ||= load_commits(st_commits) end def reload_commits @@ -117,6 +121,14 @@ class MergeRequestDiff < ActiveRecord::Base project.commit(head_commit_sha) end + def commits_sha + if @commits + commits.map(&:sha) + else + st_commits.map { |commit| commit[:id] } + end + end + def diff_refs return unless start_commit_sha || base_commit_sha diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb new file mode 100644 index 00000000000..ab597c37947 --- /dev/null +++ b/app/models/merge_requests_closing_issues.rb @@ -0,0 +1,7 @@ +class MergeRequestsClosingIssues < ActiveRecord::Base + belongs_to :merge_request + belongs_to :issue + + validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true + validates :issue_id, presence: true +end diff --git a/app/models/note.rb b/app/models/note.rb index b94e3cff2ce..f2656df028b 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -223,10 +223,6 @@ class Note < ActiveRecord::Base end end - def user_authored?(user) - user == author - end - def award_emoji? can_be_award_emoji? && contains_emoji_only? end diff --git a/app/models/project.rb b/app/models/project.rb index d7f20070be0..7265cb55594 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1293,6 +1293,22 @@ class Project < ActiveRecord::Base Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) } end + def environments_for(ref, commit, with_tags: false) + environment_ids = deployments.group(:environment_id). + select(:environment_id) + + environment_ids = + if with_tags + environment_ids.where('ref=? OR tag IS TRUE', ref) + else + environment_ids.where(ref: ref) + end + + environments.where(id: environment_ids).select do |environment| + environment.includes_commit?(commit) + end + end + private def pushes_since_gc_redis_key diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 9c602c582bd..8c9534c3565 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -22,6 +22,12 @@ class ProjectFeature < ActiveRecord::Base belongs_to :project + default_value_for :builds_access_level, value: ENABLED, allows_nil: false + default_value_for :issues_access_level, value: ENABLED, allows_nil: false + default_value_for :merge_requests_access_level, value: ENABLED, allows_nil: false + default_value_for :snippets_access_level, value: ENABLED, allows_nil: false + default_value_for :wiki_access_level, value: ENABLED, allows_nil: false + def feature_available?(feature, user) raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature) diff --git a/app/models/project_team.rb b/app/models/project_team.rb index ab6ea2aae36..d9ce5088903 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -163,7 +163,7 @@ class ProjectTeam # Each group produces a list of maximum access level per user. We take the # max of the values produced by each group. - if project.invited_groups.any? && project.allowed_to_share_with_group? + if project_shared_with_group? project.project_group_links.each do |group_link| invited_access = max_invited_level_for_users(group_link, user_ids) merge_max!(access, invited_access) @@ -200,43 +200,17 @@ class ProjectTeam def fetch_members(level = nil) project_members = project.members group_members = group ? group.members : [] - invited_members = [] - - if project.invited_groups.any? && project.allowed_to_share_with_group? - project.project_group_links.includes(group: [:group_members]).each do |group_link| - invited_group = group_link.group - im = invited_group.members - - if level - int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize] - - # Skip group members if we ask for masters - # but max group access is developers - next if int_level > group_link.group_access - - # If we ask for developers and max - # group access is developers we need to provide - # both group master, developers as devs - if int_level == group_link.group_access - im.where("access_level >= ?)", group_link.group_access) - else - im.send(level) - end - end - - invited_members << im - end - - invited_members = invited_members.flatten.compact - end if level - project_members = project_members.send(level) - group_members = group_members.send(level) if group + project_members = project_members.public_send(level) + group_members = group_members.public_send(level) if group end user_ids = project_members.pluck(:user_id) + + invited_members = fetch_invited_members(level) user_ids.push(*invited_members.map(&:user_id)) if invited_members.any? + user_ids.push(*group_members.pluck(:user_id)) if group User.where(id: user_ids) @@ -249,4 +223,38 @@ class ProjectTeam def merge_max!(first_hash, second_hash) first_hash.merge!(second_hash) { |_key, old, new| old > new ? old : new } end + + def project_shared_with_group? + project.invited_groups.any? && project.allowed_to_share_with_group? + end + + def fetch_invited_members(level = nil) + invited_members = [] + + return invited_members unless project_shared_with_group? + + project.project_group_links.includes(group: [:group_members]).each do |link| + invited_group_members = link.group.members + + if level + numeric_level = GroupMember.access_level_roles[level.to_s.singularize.titleize] + + # If we're asked for a level that's higher than the group's access, + # there's nothing left to do + next if numeric_level > link.group_access + + # Make sure we include everyone _above_ the requested level as well + invited_group_members = + if numeric_level == link.group_access + invited_group_members.where("access_level >= ?", link.group_access) + else + invited_group_members.public_send(level) + end + end + + invited_members << invited_group_members + end + + invited_members.flatten.compact + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 772c62a4124..51557228ab9 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -840,7 +840,7 @@ class Repository def get_committer_and_author(user, email: nil, name: nil) committer = user_to_committer(user) - author = name && email ? Gitlab::Git::committer_hash(email: email, name: name) : committer + author = Gitlab::Git::committer_hash(email: email, name: name) || committer { author: author, diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 5ec933601ac..8a1730f3f36 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -4,6 +4,7 @@ class Snippet < ActiveRecord::Base include Participable include Referable include Sortable + include Awardable default_value_for :visibility_level, Snippet::PRIVATE diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 00c4c7b1440..be25c750d67 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -46,6 +46,7 @@ class ProjectPolicy < BasePolicy can! :create_issue can! :create_note can! :upload_file + can! :read_cycle_analytics end def reporter_access! @@ -204,6 +205,7 @@ class ProjectPolicy < BasePolicy can! :read_commit_status can! :read_container_image can! :download_code + can! :read_cycle_analytics # NOTE: may be overridden by IssuePolicy can! :read_issue diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 98da6563947..38ac6631228 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -5,7 +5,7 @@ module Auth AUDIENCE = 'container_registry' def execute(authentication_abilities:) - @authentication_abilities = authentication_abilities || [] + @authentication_abilities = authentication_abilities return error('not found', 404) unless registry.enabled diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb index e6667132e27..799ad3e1bd0 100644 --- a/app/services/create_deployment_service.rb +++ b/app/services/create_deployment_service.rb @@ -4,7 +4,7 @@ class CreateDeploymentService < BaseService def execute(deployable = nil) environment = find_or_create_environment - project.deployments.create( + deployment = project.deployments.create( environment: environment, ref: params[:ref], tag: params[:tag], @@ -12,6 +12,10 @@ class CreateDeploymentService < BaseService user: current_user, deployable: deployable ) + + deployment.update_merge_request_metrics! + + deployment end private diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 948041063c0..c499427605a 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -134,6 +134,7 @@ class GitPushService < BaseService end commit.create_cross_references!(authors[commit], closed_issues) + update_issue_metrics(commit, authors) end end @@ -186,4 +187,11 @@ class GitPushService < BaseService def branch_name @branch_name ||= Gitlab::Git.ref_name(params[:ref]) end + + def update_issue_metrics(commit, authors) + mentioned_issues = commit.all_references(authors[commit]).issues + + Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil). + update_all(first_mentioned_in_commit_at: commit.committed_date) + end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 4c8d93999a7..fbce46769f7 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -157,6 +157,10 @@ class IssuableBaseService < BaseService # To be overridden by subclasses end + def after_update(issuable) + # To be overridden by subclasses + end + def update_issuable(issuable, attributes) issuable.with_transaction_returning_status do issuable.update(attributes.merge(updated_by: current_user)) @@ -182,6 +186,7 @@ class IssuableBaseService < BaseService end handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users) + after_update(issuable) issuable.create_new_cross_references!(current_user) execute_hooks(issuable, 'update') end diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index 73247e62421..b0ae2dfe4ce 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -20,6 +20,7 @@ module MergeRequests event_service.open_mr(issuable, current_user) notification_service.new_merge_request(issuable, current_user) todo_service.new_merge_request(issuable, current_user) + issuable.cache_merge_request_closes_issues!(current_user) end end end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 5cedd6f11d9..22596b4014a 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -13,6 +13,7 @@ module MergeRequests reload_merge_requests reset_merge_when_build_succeeds mark_pending_todos_done + cache_merge_requests_closing_issues # Leave a system note if a branch was deleted/added if branch_added? || branch_removed? @@ -141,6 +142,14 @@ module MergeRequests end end + # If the merge requests closes any issues, save this information in the + # `MergeRequestsClosingIssues` model (as a performance optimization). + def cache_merge_requests_closing_issues + @project.merge_requests.where(source_branch: @branch_name).each do |merge_request| + merge_request.cache_merge_request_closes_issues!(@current_user) + end + end + def filter_merge_requests(merge_requests) merge_requests.uniq.select(&:source_project) end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 398ec47f0ea..f14f9e4b327 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -77,5 +77,9 @@ module MergeRequests def close_service MergeRequests::CloseService end + + def after_update(issuable) + issuable.cache_merge_request_closes_issues!(current_user) + end end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index d929364fc96..0d79ca7dc52 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -49,28 +49,6 @@ = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control') %span.help-block#clone-protocol-help Allow only the selected protocols to be used for Git access. - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :version_check_enabled do - = f.check_box :version_check_enabled - Version check enabled - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :email_author_in_body do - = f.check_box :email_author_in_body - Include author name in notification email body - .help-block - Some email servers do not support overriding the email sender name. - Enable this option to include the name of the author of the issue, - merge request or comment in the email body instead. - .form-group - = f.label :admin_notification_email, class: 'control-label col-sm-2' - .col-sm-10 - = f.text_field :admin_notification_email, class: 'form-control' - .help-block - Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. %fieldset %legend Account and Limit Settings @@ -341,6 +319,15 @@ %a{ href: 'http://www.akismet.com', target: 'blank'} http://www.akismet.com %fieldset + %legend Abuse reports + .form-group + = f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :admin_notification_email, class: 'form-control' + .help-block + Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. + + %fieldset %legend Error Reporting and Logging %p These settings require a restart to take effect. @@ -407,6 +394,29 @@ = succeed "." do = link_to "Koding administration documentation", help_page_path("administration/integration/koding") + %fieldset + %legend Usage statistics + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :version_check_enabled do + = f.check_box :version_check_enabled + Version check enabled + .help-block + Let GitLab inform you when an update is available. + + %fieldset + %legend Email + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :email_author_in_body do + = f.check_box :email_author_in_body + Include author name in notification email body + .help-block + Some email servers do not support overriding the email sender name. + Enable this option to include the name of the author of the issue, + merge request or comment in the email body instead. .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index 02efcecc889..fbe3ab912b6 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -1,5 +1,5 @@ - grouped_emojis = awardable.grouped_awards(with_thumbs: inline) -.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } } +.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } } - awards_sort(grouped_emojis).each do |emoji, awards| %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } } = emoji_icon(emoji, sprite: false) diff --git a/app/views/discussions/_jump_to_next.html.haml b/app/views/discussions/_jump_to_next.html.haml index da970792b4d..7ed09dd1a98 100644 --- a/app/views/discussions/_jump_to_next.html.haml +++ b/app/views/discussions/_jump_to_next.html.haml @@ -1,4 +1,3 @@ -- diff_notes_disabled = (@merge_request_diff.latest? && !!@start_sha) if @merge_request_diff - discussion = local_assigns.fetch(:discussion, nil) - if current_user %jump-to-discussion{ "inline-template" => true, ":discussion-id" => "'#{discussion.try(:id)}'" } @@ -6,6 +5,5 @@ %button.btn.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion", title: "Jump to next unresolved discussion", "aria-label" => "Jump to next unresolved discussion", - data: { container: "body" }, - disabled: diff_notes_disabled } + data: { container: "body" }} = custom_icon("next_discussion") diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 53ed4fa991d..31db6ee0cad 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -23,7 +23,7 @@ .cover-desc.description = markdown(@group.description, pipeline: :description) -%div{ class: container_class } +%div.groups-header{ class: container_class } .top-area %ul.nav-links %li.active diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 8e4937b7aa0..e44a2bfed9d 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -47,7 +47,7 @@ Repository - if project_nav_tab? :pipelines - = nav_link(controller: [:pipelines, :builds, :environments]) do + = nav_link(controller: [:pipelines, :builds, :environments, :cycle_analytics]) do = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do %span Pipelines diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 9fe94291db7..277eb71ea73 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -14,9 +14,6 @@ window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}"; window.preview_markdown_path = "#{preview_markdown_path}"; -- content_for :scripts_body do - = render "layouts/init_auto_complete" if current_user - - content_for :header_content do .js-dropdown-menu-projects .dropdown-menu.dropdown-select.dropdown-menu-projects diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index 3978fa60d66..cb97181b9e1 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -7,3 +7,6 @@ = text_area_tag attr, nil, class: classes, placeholder: placeholder %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" } = icon('compress') + +- content_for :scripts_body do + = render "layouts/init_auto_complete" if current_user && (@target_project || @project) diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml index 61eff73da26..c2bcfb773a6 100644 --- a/app/views/projects/builds/_table.html.haml +++ b/app/views/projects/builds/_table.html.haml @@ -9,7 +9,7 @@ %thead %tr %th Status - %th Commit + %th Build - if admin %th Project %th Runner diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml new file mode 100644 index 00000000000..7f346df8797 --- /dev/null +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -0,0 +1,59 @@ +- @no_container = true +- page_title "Cycle Analytics" += render "projects/pipelines/head" + +#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project)}} + + .bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"} + = icon('times', class: 'dismiss-icon', "@click": "dismissLanding()") + .row + .col-sm-3.col-xs-12.svg-container + = custom_icon('icon_cycle_analytics_splash') + .col-sm-8.col-xs-12.inner-content + %h4 + Introducing Cycle Analytics + %p + Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project. + + = link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn' + + = icon("spinner spin", "v-show" => "isLoading") + + .wrapper{"v-show" => "!isLoading && !hasError"} + .panel.panel-default + .panel-heading + Pipeline Health + + .content-block + .container-fluid + .row + .col-sm-3.col-xs-12.column{"v-for" => "item in analytics.summary"} + %h3.header {{item.value}} + %p.text {{item.title}} + + .col-sm-3.col-xs-12.column + .dropdown.inline.js-ca-dropdown + %button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"} + %span.dropdown-label Last 30 days + %i.fa.fa-chevron-down + %ul.dropdown-menu.dropdown-menu-align-right + %li + %a{'href' => "#", 'data-value' => '30'} + Last 30 days + %li + %a{'href' => "#", 'data-value' => '90'} + Last 90 days + + .bordered-box + %ul.content-list + %li{"v-for" => "item in analytics.stats"} + .container-fluid + .row + .col-xs-8.title-col + %p.title + {{item.title}} + %p.text + {{item.description}} + .col-xs-4.value-col + %span + {{item.value}} diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml index 8f7b5d1543e..49819519759 100644 --- a/app/views/projects/merge_requests/show/_versions.html.haml +++ b/app/views/projects/merge_requests/show/_versions.html.haml @@ -10,23 +10,25 @@ - else version #{version_index(@merge_request_diff)} %span.caret - %ul.dropdown-menu.dropdown-menu-selectable + .dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-title %span Version: - %button.dropdown-title-button.dropdown-menu-close - %i.fa.fa-times.dropdown-menu-close-icon - - @merge_request_diffs.each do |merge_request_diff| - %li - = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do - %strong - - if merge_request_diff.latest? - latest version - - else - version #{version_index(merge_request_diff)} - .monospace #{short_sha(merge_request_diff.head_commit_sha)} - %small - #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)}, - = time_ago_with_tooltip(merge_request_diff.created_at) + %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + = icon('times', class: 'dropdown-menu-close-icon') + .dropdown-content + %ul + - @merge_request_diffs.each do |merge_request_diff| + %li + = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do + %strong + - if merge_request_diff.latest? + latest version + - else + version #{version_index(merge_request_diff)} + .monospace #{short_sha(merge_request_diff.head_commit_sha)} + %small + #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)}, + = time_ago_with_tooltip(merge_request_diff.created_at) - if @merge_request_diff.base_commit_sha and @@ -38,27 +40,29 @@ - else #{@merge_request.target_branch} %span.caret - %ul.dropdown-menu.dropdown-menu-selectable + .dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-title %span Compared with: - %button.dropdown-title-button.dropdown-menu-close - %i.fa.fa-times.dropdown-menu-close-icon - - @comparable_diffs.each do |merge_request_diff| - %li - = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do - %strong - - if merge_request_diff.latest? - latest version - - else - version #{version_index(merge_request_diff)} - .monospace #{short_sha(merge_request_diff.head_commit_sha)} - %small - = time_ago_with_tooltip(merge_request_diff.created_at) - %li - = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do - %strong - #{@merge_request.target_branch} (base) - .monospace #{short_sha(@merge_request_diff.base_commit_sha)} + %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + = icon('times', class: 'dropdown-menu-close-icon') + .dropdown-content + %ul + - @comparable_diffs.each do |merge_request_diff| + %li + = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do + %strong + - if merge_request_diff.latest? + latest version + - else + version #{version_index(merge_request_diff)} + .monospace #{short_sha(merge_request_diff.head_commit_sha)} + %small + = time_ago_with_tooltip(merge_request_diff.created_at) + %li + = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do + %strong + #{@merge_request.target_branch} (base) + .monospace #{short_sha(@merge_request_diff.base_commit_sha)} - unless @merge_request_diff.latest? && !@start_sha .comments-disabled-notif.content-block @@ -67,4 +71,4 @@ Comments are disabled because you're comparing two versions of this merge request. - else Comments are disabled because you're viewing an old version of this merge request. - = link_to 'Show latest version', merge_request_version_path(@project, @merge_request, @merge_request_diff), class: 'btn btn-sm' + = link_to 'Show latest version', diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-sm' diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 494695a03a5..44e645a7e81 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -43,15 +43,16 @@ = icon("times-circle") Could not connect to the CI server. Please check your settings and try again. -- @merge_request.environments.each do |environment| - .mr-widget-heading - .ci_widget.ci-success - = ci_icon_for_status("success") - %span.hidden-sm - Deployed to - = succeed '.' do - = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment), class: 'environment' - - external_url = environment.external_url - - if external_url - = link_to external_url, target: '_blank' do - = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true) +- @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.hidden-sm + 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 + = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true) diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 74538a9723e..8352eba7446 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -14,9 +14,9 @@ .disabled-comment.text-center .disabled-comment-text.inline Please - = link_to "register", new_session_path(:user, redirect_to_referer: 'yes') + = link_to "sign up", new_session_path(:user, redirect_to_referer: 'yes') or - = link_to "login", new_session_path(:user, redirect_to_referer: 'yes') + = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes') to post a comment :javascript diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index f611ddc8f5f..5f571499e80 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -19,3 +19,9 @@ = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do %span Environments + + - if can?(current_user, :read_cycle_analytics, @project) + = nav_link(controller: %w(cycle_analytics)) do + = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics' do + %span + Cycle Analytics diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml index 0628134b1bb..0193800dedf 100644 --- a/app/views/projects/protected_branches/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_protected_branch.html.haml @@ -1,4 +1,4 @@ -%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch), branch_id: protected_branch.id } } +%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) } } %td = protected_branch.name - if @project.root_ref?(protected_branch.name) diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index a5a5619fa12..4aa4ab46a2f 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -3,7 +3,7 @@ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New Snippet" do New Snippet - if can?(current_user, :update_project_snippet, @snippet) - = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-warning", title: 'Delete Snippet' do + = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do Delete - if can?(current_user, :update_project_snippet, @snippet) = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index b70fda88a79..9503dbded13 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -2,13 +2,16 @@ = render 'shared/snippets/header' -%article.file-holder.snippet-file-content - .file-title - = blob_icon 0, @snippet.file_name - = @snippet.file_name - .file-actions - = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") - = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" - = render 'shared/snippets/blob' - -%div#notes= render "projects/notes/notes_with_form" +.project-snippets + %article.file-holder.snippet-file-content + .file-title + = blob_icon 0, @snippet.file_name + = @snippet.file_name + .file-actions + = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") + = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" + = render 'shared/snippets/blob' + + = render 'award_emoji/awards_block', awardable: @snippet, inline: true + + %div#notes= render "projects/notes/notes_with_form" diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml index cf16c203f9c..73d288e2236 100644 --- a/app/views/shared/_milestones_filter.html.haml +++ b/app/views/shared/_milestones_filter.html.haml @@ -1,10 +1,19 @@ +- if @project + - counts = milestone_counts(@project.milestones) + %ul.nav-links - %li{class: ("active" if params[:state].blank? || params[:state] == 'opened')} + %li{class: milestone_class_for_state(params[:state], 'opened', true)} = link_to milestones_filter_path(state: 'opened') do Open - %li{class: ("active" if params[:state] == 'closed')} + - if @project + %span.badge #{counts[:opened]} + %li{class: milestone_class_for_state(params[:state], 'closed')} = link_to milestones_filter_path(state: 'closed') do Closed - %li{class: ("active" if params[:state] == 'all')} + - if @project + %span.badge #{counts[:closed]} + %li{class: milestone_class_for_state(params[:state], 'all')} = link_to milestones_filter_path(state: 'all') do All + - if @project + %span.badge #{counts[:all]} diff --git a/app/views/shared/icons/_icon_cycle_analytics_splash.svg b/app/views/shared/icons/_icon_cycle_analytics_splash.svg new file mode 100644 index 00000000000..eb5a962d651 --- /dev/null +++ b/app/views/shared/icons/_icon_cycle_analytics_splash.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 99 102" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="0" d="m35.12 56.988c4.083-4.385 5.968-12.155 5.968-24.04 0-20.2-15.874-32.16-15.874-32.16-1.114-.954-2.929-.979-4.04 0 0 0-15.874 11.957-15.874 32.16 0 11.882 1.884 19.652 5.968 24.04h23.848"/><mask id="1" width="35.783" height="56.924" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(0-4)"><g transform="translate(32.15 3.976)"><g fill="#6b4fbb"><path d="m11.928 56.988l1.325-1.325v3.313c0 .737.59 1.325 1.325 1.325h17.229c.736 0 1.325-.59 1.325-1.325v-3.313l1.325 1.325h-22.53m22.53-1.325v3.313c0 1.464-1.18 2.651-2.651 2.651h-17.229c-1.464 0-2.651-1.178-2.651-2.651v-3.313h22.53m-5.964 7.361h.663c0 3.294-2.67 5.964-5.964 5.964-3.294 0-5.964-2.67-5.964-5.964h.663.663c0 2.562 2.077 4.639 4.639 4.639 2.562 0 4.639-2.077 4.639-4.639h.663"/><path d="m5.816 42.535c-.346-2.839-.515-6.03-.515-9.584 0-20.2 15.874-32.16 15.874-32.16 1.106-.979 2.921-.954 4.04 0 0 0 15.874 11.957 15.874 32.16 0 11.882-1.884 19.652-5.968 24.04h-23.848c-2.861-3.073-4.643-7.807-5.453-14.453-.06-.493-.115-.997-.164-1.511l-4.04 2.884c-.891.637-1.614 2.041-1.614 3.137v14.581c0 1.465.971 1.958 2.165 1.106l8.691-6.208c-.282-.332-.553-.681-.813-1.048l-8.648 6.177c-.147.105-.069.152-.069-.027v-14.581c0-.668.516-1.671 1.059-2.059l3.432-2.451m38.4 20.2c1.193.852 2.165.359 2.165-1.106v-14.581c0-1.096-.723-2.5-1.614-3.137l-4.04-2.884c-.049.514-.104 1.018-.164 1.511l3.432 2.451c.543.388 1.059 1.391 1.059 2.059v14.581c0 .179.078.132-.069.027l-8.648-6.177c-.26.367-.531.716-.813 1.048l8.691 6.208"/></g><use fill="#fff" stroke="#6b4fbb" stroke-width="2.651" mask="url(#1)" xlink:href="#0"/><g fill="#b5a7dd"><path d="m30.482 28.494c0-4.03-3.263-7.289-7.289-7.289-4.03 0-7.289 3.263-7.289 7.289 0 4.03 3.263 7.289 7.289 7.289 4.03 0 7.289-3.263 7.289-7.289m-15.904 0c0-4.758 3.857-8.614 8.614-8.614 4.758 0 8.614 3.857 8.614 8.614 0 4.758-3.857 8.614-8.614 8.614-4.758 0-8.614-3.857-8.614-8.614"/><path d="m27.17 28.494c0-2.196-1.78-3.976-3.976-3.976-2.196 0-3.976 1.78-3.976 3.976 0 2.196 1.78 3.976 3.976 3.976 2.196 0 3.976-1.78 3.976-3.976m-9.277 0c0-2.928 2.373-5.301 5.301-5.301 2.928 0 5.301 2.373 5.301 5.301 0 2.928-2.373 5.301-5.301 5.301-2.928 0-5.301-2.373-5.301-5.301"/></g><path fill="#6b4fbb" d="m34.458 87.47c0 1.098.89 1.988 1.988 1.988 1.098 0 1.988-.89 1.988-1.988 0-.366.297-.663.663-.663.366 0 .663.297.663.663 0 1.83-1.483 3.313-3.313 3.313-1.826 0-3.307-1.478-3.313-3.302 0-.002 0-.003 0-.005v-2.663c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.657m-21.2-6.615c0-.002 0-.003 0-.005v-2.663c0-.358-.297-.657-.663-.657-.369 0-.663.294-.663.657v2.657c0 1.098-.89 1.988-1.988 1.988-1.098 0-1.988-.89-1.988-1.988 0-.366-.297-.663-.663-.663-.366 0-.663.297-.663.663 0 1.83 1.483 3.313 3.313 3.313 1.826 0 3.307-1.477 3.313-3.302m5.301 7.285c0-.001 0-.002 0-.003v-16.576c0-.362-.297-.658-.663-.658-.369 0-.663.295-.663.658v16.571c0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645 0-.366-.297-.663-.663-.663-.366 0-.663.297-.663.663 0 2.745 2.225 4.97 4.97 4.97 2.742 0 4.966-2.221 4.97-4.963m10.602 8.607v-18.555c0-.365-.297-.661-.663-.661-.369 0-.663.296-.663.661v18.557c0 0 0 0 0 .001.001 2.744 2.226 4.968 4.97 4.968 2.745 0 4.97-2.225 4.97-4.97 0-.366-.297-.663-.663-.663-.366 0-.663.297-.663.663 0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645m3.976-25.19c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m0 6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m-10.602-6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m5.301 0c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m-5.301 6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m0 6.627c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663m-10.602-13.253c0-.363.294-.657.663-.657.366 0 .663.299.663.657v2.663c0 .363-.294.657-.663.657-.366 0-.663-.299-.663-.657v-2.663"/></g><path fill="#e2ddf2" d="m97.75 76.54c0-2.745-2.225-4.97-4.97-4.97-2.745 0-4.97 2.225-4.97 4.97 0 2.745 2.225 4.97 4.97 4.97 2.745 0 4.97-2.225 4.97-4.97m-8.614 0c0-2.01 1.632-3.645 3.645-3.645 2.01 0 3.645 1.632 3.645 3.645 0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645m-60.964-57.651c0-2.745-2.225-4.97-4.97-4.97-2.745 0-4.97 2.225-4.97 4.97 0 2.745 2.225 4.97 4.97 4.97 2.745 0 4.97-2.225 4.97-4.97m-8.614 0c0-2.01 1.632-3.645 3.645-3.645 2.01 0 3.645 1.632 3.645 3.645 0 2.01-1.632 3.645-3.645 3.645-2.01 0-3.645-1.632-3.645-3.645"/><path fill="#b5a7dd" d="m98.41 34.458c0-1.83-1.483-3.313-3.313-3.313-1.83 0-3.313 1.483-3.313 3.313 0 1.83 1.483 3.313 3.313 3.313 1.83 0 3.313-1.483 3.313-3.313m-5.301 0c0-1.098.89-1.988 1.988-1.988 1.098 0 1.988.89 1.988 1.988 0 1.098-.89 1.988-1.988 1.988-1.098 0-1.988-.89-1.988-1.988m-86.14 20.542c0-1.83-1.483-3.313-3.313-3.313-1.83 0-3.313 1.483-3.313 3.313 0 1.83 1.483 3.313 3.313 3.313 1.83 0 3.313-1.483 3.313-3.313m-5.301 0c0-1.098.89-1.988 1.988-1.988 1.098 0 1.988.89 1.988 1.988 0 1.098-.89 1.988-1.988 1.988-1.098 0-1.988-.89-1.988-1.988"/></g></svg> diff --git a/app/views/shared/icons/_icon_fork.svg b/app/views/shared/icons/_icon_fork.svg index fc970e4ce50..ce22b6cdaea 100644 --- a/app/views/shared/icons/_icon_fork.svg +++ b/app/views/shared/icons/_icon_fork.svg @@ -1,3 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="30" height="40" viewBox="5 0 30 40"> - <path fill="#7E7E7E" fill-rule="evenodd" d="M22,29.5351288 L22,22.7193602 C26.1888699,21.5098039 29.3985457,16.802989 29.3985457,16.802989 C29.740988,16.3567547 30,15.5559546 30,15.0081969 L30,10.4648712 C31.1956027,9.77325238 32,8.48056471 32,7 C32,4.790861 30.209139,3 28,3 C25.790861,3 24,4.790861 24,7 C24,8.48056471 24.8043973,9.77325238 26,10.4648712 L26,14.7083871 C26,14.8784435 25.9055559,15.0987329 25.7890533,15.2104147 C25.7890533,15.2104147 24.5373893,16.4126202 23.9488702,16.9515733 C22.5015398,18.2770075 21.1191354,19 20.090554,19 C19.0477772,19 17.6172728,18.2608988 16.1128852,16.9142923 C15.5030182,16.3683886 14.3672121,15.3403307 14.3672121,15.3403307 C14.1659605,15.1583364 14.0000086,14.7846305 14.0000192,14.5088473 C14.0000192,14.5088473 14.0000932,12.7539451 14.0001308,10.4647956 C15.1956614,9.77315812 16,8.48051074 16,7 C16,4.790861 14.209139,3 12,3 C9.790861,3 8,4.790861 8,7 C8,8.48056471 8.80439726,9.77325238 10,10.4648712 L10,15.0081969 C10,15.5446944 10.2736352,16.3534183 10.6111812,16.7893819 C10.6111812,16.7893819 13.8599776,21.3779363 18,22.6668724 L18,29.5351288 C16.8043973,30.2267476 16,31.5194353 16,33 C16,35.209139 17.790861,37 20,37 C22.209139,37 24,35.209139 24,33 C24,31.5194353 23.1956027,30.2267476 22,29.5351288 Z M14,7 C14,5.8954305 13.1045695,5 12,5 C10.8954305,5 10,5.8954305 10,7 C10,8.1045695 10.8954305,9 12,9 C13.1045695,9 14,8.1045695 14,7 Z M30,7 C30,5.8954305 29.1045695,5 28,5 C26.8954305,5 26,5.8954305 26,7 C26,8.1045695 26.8954305,9 28,9 C29.1045695,9 30,8.1045695 30,7 Z M22,33 C22,31.8954305 21.1045695,31 20,31 C18.8954305,31 18,31.8954305 18,33 C18,34.1045695 18.8954305,35 20,35 C21.1045695,35 22,34.1045695 22,33 Z"/> -</svg> +<svg xmlns="http://www.w3.org/2000/svg" width="30" height="40" viewBox="5 0 30 40"><path fill="#7E7E7E" fill-rule="evenodd" d="M22 29.535V22.72c4.19-1.21 7.4-5.917 7.4-5.917.34-.446.6-1.247.6-1.795v-4.543C31.196 9.773 32 8.48 32 7c0-2.21-1.79-4-4-4s-4 1.79-4 4c0 1.48.804 2.773 2 3.465v4.243c0 .17-.094.39-.21.502 0 0-1.253 1.203-1.84 1.742C22.5 18.277 21.12 19 20.09 19c-1.042 0-2.473-.74-3.977-2.086-.61-.546-1.746-1.574-1.746-1.574-.2-.182-.367-.555-.367-.83v-4.045C15.196 9.773 16 8.48 16 7c0-2.21-1.79-4-4-4S8 4.79 8 7c0 1.48.804 2.773 2 3.465v4.543c0 .537.274 1.345.61 1.78 0 0 3.25 4.59 7.39 5.88v6.867c-1.196.692-2 1.984-2 3.465 0 2.21 1.79 4 4 4s4-1.79 4-4c0-1.48-.804-2.773-2-3.465zM14 7c0-1.105-.895-2-2-2s-2 .895-2 2 .895 2 2 2 2-.895 2-2zm16 0c0-1.105-.895-2-2-2s-2 .895-2 2 .895 2 2 2 2-.895 2-2zm-8 26c0-1.105-.895-2-2-2s-2 .895-2 2 .895 2 2 2 2-.895 2-2z"/></svg> diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 93c4d5c3d30..cf26197f7d7 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -2,9 +2,9 @@ .issues-filters .issues-details-filters.row-content-block.second-block - = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search]), method: :get, class: 'filter-form js-filter-form' do - - if params[:issue_search].present? - = hidden_field_tag :issue_search, params[:issue_search] + = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do + - if params[:search].present? + = hidden_field_tag :search, params[:search] - if @bulk_edit .check-all-holder = check_box_tag "check_all_issues", nil, false, @@ -29,7 +29,7 @@ = render "shared/issuable/label_dropdown" .filter-item.inline.reset-filters - %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search])} Reset filters + %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters .pull-right - if boards_page diff --git a/app/views/shared/issuable/_search_form.html.haml b/app/views/shared/issuable/_search_form.html.haml index 186963b32b8..2c89217cadd 100644 --- a/app/views/shared/issuable/_search_form.html.haml +++ b/app/views/shared/issuable/_search_form.html.haml @@ -1,2 +1,2 @@ -= form_tag(path, method: :get, id: "issue_search_form", class: 'issue-search-form') do - = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by name ...', class: 'form-control issue_search search-text-input input-short', spellcheck: false } += form_tag(path, method: :get, id: "issuable_search_form", class: 'issuable-search-form') do + = search_field_tag :search, params[:search], { id: 'issuable_search', placeholder: 'Filter by name ...', class: 'form-control issuable_search search-text-input input-short', spellcheck: false } diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index acc3ccf4dcf..3dccfb147bf 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -33,7 +33,7 @@ - if @project .row .col-sm-6= render('shared/milestone_expired', milestone: milestone) - .col-sm-6 + .col-sm-6.milestone-actions - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs btn-grouped" do Edit diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index fa403da8f79..cd89155c616 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -10,3 +10,5 @@ = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" = render 'shared/snippets/blob' + += render 'award_emoji/awards_block', awardable: @snippet, inline: true
\ No newline at end of file diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 9a052abe40a..2a57ac90bab 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -10,72 +10,72 @@ = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") .user-profile - .cover-block + .cover-block.user-cover-block .cover-controls - if @user == current_user = link_to profile_path, class: 'btn btn-gray' do = icon('pencil') - elsif current_user - %span.report-abuse - - if @user.abuse_report - %button.btn.btn-danger{ title: 'Already reported for abuse', - data: { toggle: 'tooltip', placement: 'left', container: 'body' }} - = icon('exclamation-circle') - - else - = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray', - title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do - = icon('exclamation-circle') + - if @user.abuse_report + %button.btn.btn-danger{ title: 'Already reported for abuse', + data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }} + = icon('exclamation-circle') + - else + = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray', + title: 'Report abuse', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('exclamation-circle') - if current_user - = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do = icon('rss') - if current_user.admin? - = link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('users') - .avatar-holder - = link_to avatar_icon(@user, 400), target: '_blank' do - = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: '' - .cover-title - = @user.name - - .cover-desc - %span.middle-dot-divider - @#{@user.username} - %span.middle-dot-divider - Member since #{@user.created_at.to_s(:medium)} + .profile-header + .avatar-holder + = link_to avatar_icon(@user, 400), target: '_blank' do + = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: '' + + .user-info + .cover-title + = @user.name + %span.handle + @#{@user.username} + + .cover-desc.member-date + %span.middle-dot-divider + Member since #{@user.created_at.to_s(:medium)} + + .cover-desc + - unless @user.public_email.blank? + .profile-link-holder.middle-dot-divider + = link_to @user.public_email, "mailto:#{@user.public_email}" + - unless @user.skype.blank? + .profile-link-holder.middle-dot-divider + = link_to "skype:#{@user.skype}", title: "Skype" do + = icon('skype') + - unless @user.linkedin.blank? + .profile-link-holder.middle-dot-divider + = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do + = icon('linkedin-square') + - unless @user.twitter.blank? + .profile-link-holder.middle-dot-divider + = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do + = icon('twitter-square') + - unless @user.website_url.blank? + .profile-link-holder.middle-dot-divider + = link_to @user.short_website_url, @user.full_website_url + - unless @user.location.blank? + .profile-link-holder.middle-dot-divider + = icon('map-marker') + = @user.location - if @user.bio.present? .cover-desc %p.profile-user-bio = @user.bio - .cover-desc - - unless @user.public_email.blank? - .profile-link-holder.middle-dot-divider - = link_to @user.public_email, "mailto:#{@user.public_email}" - - unless @user.skype.blank? - .profile-link-holder.middle-dot-divider - = link_to "skype:#{@user.skype}", title: "Skype" do - = icon('skype') - - unless @user.linkedin.blank? - .profile-link-holder.middle-dot-divider - = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do - = icon('linkedin-square') - - unless @user.twitter.blank? - .profile-link-holder.middle-dot-divider - = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do - = icon('twitter-square') - - unless @user.website_url.blank? - .profile-link-holder.middle-dot-divider - = link_to @user.short_website_url, @user.full_website_url - - unless @user.location.blank? - .profile-link-holder.middle-dot-divider - = icon('map-marker') - = @user.location - %ul.nav-links.center.user-profile-nav %li.js-activity-tab = link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do |