diff options
Diffstat (limited to 'app')
89 files changed, 1057 insertions, 1030 deletions
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js deleted file mode 100644 index 00da5f17f9f..00000000000 --- a/app/assets/javascripts/diff.js +++ /dev/null @@ -1,73 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, max-len, one-var, camelcase, one-var-declaration-per-line, no-unused-vars, no-unused-expressions, no-sequences, object-shorthand, comma-dangle, prefer-arrow-callback, semi, radix, padded-blocks, max-len */ -(function() { - this.Diff = (function() { - var UNFOLD_COUNT; - - UNFOLD_COUNT = 20; - - function Diff() { - $('.files .diff-file').singleFileDiff(); - this.filesCommentButton = $('.files .diff-file').filesCommentButton(); - if (this.diffViewType() === 'parallel') { - $('.content-wrapper .container-fluid').removeClass('container-limited'); - } - $(document).off('click', '.js-unfold'); - $(document).on('click', '.js-unfold', (function(_this) { - return function(event) { - var line_number, link, file, offset, old_line, params, prev_new_line, prev_old_line, ref, ref1, since, target, to, unfold, unfoldBottom; - target = $(event.target); - unfoldBottom = target.hasClass('js-unfold-bottom'); - unfold = true; - ref = _this.lineNumbers(target.parent()), old_line = ref[0], line_number = ref[1]; - offset = line_number - old_line; - if (unfoldBottom) { - line_number += 1; - since = line_number; - to = line_number + UNFOLD_COUNT; - } else { - ref1 = _this.lineNumbers(target.parent().prev()), prev_old_line = ref1[0], prev_new_line = ref1[1]; - line_number -= 1; - to = line_number; - if (line_number - UNFOLD_COUNT > prev_new_line + 1) { - since = line_number - UNFOLD_COUNT; - } else { - since = prev_new_line + 1; - unfold = false; - } - } - file = target.parents('.diff-file'); - link = file.data('blob-diff-path'); - params = { - since: since, - to: to, - bottom: unfoldBottom, - offset: offset, - unfold: unfold, - view: file.data('view') - }; - return $.get(link, params, function(response) { - return target.parent().replaceWith(response); - }); - }; - })(this)); - } - - Diff.prototype.diffViewType = function() { - return $('.inline-parallel-buttons a.active').data('view-type'); - } - - Diff.prototype.lineNumbers = function(line) { - if (!line.children().length) { - return [0, 0]; - } - - return line.find('.diff-line-num').map(function() { - return parseInt($(this).data('linenumber')); - }); - }; - - return Diff; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 new file mode 100644 index 00000000000..ecf9d1de81c --- /dev/null +++ b/app/assets/javascripts/diff.js.es6 @@ -0,0 +1,109 @@ +/* eslint-disable class-methods-use-this */ + +(() => { + const UNFOLD_COUNT = 20; + + class Diff { + constructor() { + $('.files .diff-file').singleFileDiff(); + $('.files .diff-file').filesCommentButton(); + + if (this.diffViewType() === 'parallel') { + $('.content-wrapper .container-fluid').removeClass('container-limited'); + } + + $(document) + .off('click', '.js-unfold, .diff-line-num a') + .on('click', '.js-unfold', this.handleClickUnfold.bind(this)) + .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); + + this.highlighSelectedLine(); + } + + handleClickUnfold(e) { + const $target = $(e.target); + // current babel config relies on iterators implementation, so we cannot simply do: + // const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent()); + const ref = this.lineNumbers($target.parent()); + const oldLineNumber = ref[0]; + const newLineNumber = ref[1]; + const offset = newLineNumber - oldLineNumber; + const bottom = $target.hasClass('js-unfold-bottom'); + let since; + let to; + let unfold = true; + + if (bottom) { + const lineNumber = newLineNumber + 1; + since = lineNumber; + to = lineNumber + UNFOLD_COUNT; + } else { + const lineNumber = newLineNumber - 1; + since = lineNumber - UNFOLD_COUNT; + to = lineNumber; + + // make sure we aren't loading more than we need + const prevNewLine = this.lineNumbers($target.parent().prev())[1]; + if (since <= prevNewLine + 1) { + since = prevNewLine + 1; + unfold = false; + } + } + + const file = $target.parents('.diff-file'); + const link = file.data('blob-diff-path'); + const view = file.data('view'); + + const params = { since, to, bottom, offset, unfold, view }; + $.get(link, params, response => $target.parent().replaceWith(response)); + } + + openAnchoredDiff(anchoredDiff, cb) { + const diffTitle = $(`#file-path-${anchoredDiff}`); + const diffFile = diffTitle.closest('.diff-file'); + const nothingHereBlock = $('.nothing-here-block:visible', diffFile); + if (nothingHereBlock.length) { + diffFile.singleFileDiff(true, cb); + } else { + cb(); + } + } + + handleClickLineNum(e) { + const hash = $(e.currentTarget).attr('href'); + e.preventDefault(); + if (window.history.pushState) { + window.history.pushState(null, null, hash); + } else { + window.location.hash = hash; + } + this.highlighSelectedLine(); + } + + diffViewType() { + return $('.inline-parallel-buttons a.active').data('view-type'); + } + + lineNumbers(line) { + if (!line.children().length) { + return [0, 0]; + } + return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10)); + } + + highlighSelectedLine() { + const $diffFiles = $('.diff-file'); + $diffFiles.find('.hll').removeClass('hll'); + + if (window.location.hash !== '') { + const hash = window.location.hash.replace('#', ''); + $diffFiles + .find(`tr#${hash}:not(.match) td, td#${hash}, td[data-line-code="${hash}"]`) + .addClass('hll'); + } + } + } + + window.gl = window.gl || {}; + window.gl.Diff = Diff; +})(); diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index ab521c6c1fc..3a7c5ff3681 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -61,7 +61,7 @@ new ZenMode(); break; case 'projects:compare:show': - new Diff(); + new gl.Diff(); break; case 'projects:issues:new': case 'projects:issues:edit': @@ -74,7 +74,7 @@ break; case 'projects:merge_requests:new': case 'projects:merge_requests:edit': - new Diff(); + new gl.Diff(); shortcut_handler = new ShortcutsNavigation(); new GLForm($('.merge-request-form')); new IssuableForm($('.merge-request-form')); @@ -91,7 +91,7 @@ new GLForm($('.release-form')); break; case 'projects:merge_requests:show': - new Diff(); + new gl.Diff(); shortcut_handler = new ShortcutsIssuable(true); new ZenMode(); new MergedButtons(); @@ -101,7 +101,7 @@ new MergedButtons(); break; case "projects:merge_requests:diffs": - new Diff(); + new gl.Diff(); new ZenMode(); new MergedButtons(); break; @@ -117,7 +117,7 @@ break; case 'projects:commit:show': new Commit(); - new Diff(); + new gl.Diff(); new ZenMode(); shortcut_handler = new ShortcutsNavigation(); break; diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 29cba1a49dd..8fa80502d92 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -97,6 +97,19 @@ return $('body').data('page').split(':')[0]; }; + gl.utils.parseUrl = function (url) { + var parser = document.createElement('a'); + parser.href = url; + return parser; + }; + + gl.utils.parseUrlPathname = function (url) { + var parsedUrl = gl.utils.parseUrl(url); + // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11 + // We have to make sure we always have an absolute path. + return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname; + }; + gl.utils.isMetaKey = function(e) { return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; }; diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index a4b4db14db8..88c3636be6c 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -40,7 +40,7 @@ if (window.mrTabs) { window.mrTabs.unbindEvents(); } - window.mrTabs = new MergeRequestTabs(this.opts); + window.mrTabs = new gl.MergeRequestTabs(this.opts); }; MergeRequest.prototype.showAllCommits = function() { diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js deleted file mode 100644 index 4a192ca5796..00000000000 --- a/app/assets/javascripts/merge_request_tabs.js +++ /dev/null @@ -1,443 +0,0 @@ -/* eslint-disable max-len, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-undef, one-var, one-var-declaration-per-line, quotes, comma-dangle, consistent-return, prefer-template, no-param-reassign, camelcase, vars-on-top, space-in-parens, curly, prefer-arrow-callback, no-unused-vars, no-return-assign, semi, object-shorthand, operator-assignment, padded-blocks, max-len */ -// MergeRequestTabs -// -// Handles persisting and restoring the current tab selection and lazily-loading -// content on the MergeRequests#show page. -// -/*= require js.cookie */ - -// -// ### Example Markup -// -// <ul class="nav-links merge-request-tabs"> -// <li class="notes-tab active"> -// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1"> -// Discussion -// </a> -// </li> -// <li class="commits-tab"> -// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits"> -// Commits -// </a> -// </li> -// <li class="diffs-tab"> -// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs"> -// Diffs -// </a> -// </li> -// </ul> -// -// <div class="tab-content"> -// <div class="notes tab-pane active" id="notes"> -// Notes Content -// </div> -// <div class="commits tab-pane" id="commits"> -// Commits Content -// </div> -// <div class="diffs tab-pane" id="diffs"> -// Diffs Content -// </div> -// </div> -// -// <div class="mr-loading-status"> -// <div class="loading"> -// Loading Animation -// </div> -// </div> -// -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.MergeRequestTabs = (function() { - MergeRequestTabs.prototype.diffsLoaded = false; - - MergeRequestTabs.prototype.buildsLoaded = false; - - MergeRequestTabs.prototype.pipelinesLoaded = false; - - MergeRequestTabs.prototype.commitsLoaded = false; - - MergeRequestTabs.prototype.fixedLayoutPref = null; - - function MergeRequestTabs(opts) { - this.opts = opts != null ? opts : {}; - this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true; - - this.buildsLoaded = this.opts.buildsLoaded || false; - - this.setCurrentAction = bind(this.setCurrentAction, this); - this.tabShown = bind(this.tabShown, this); - this.showTab = bind(this.showTab, this); - // Store the `location` object, allowing for easier stubbing in tests - this._location = location; - this.bindEvents(); - this.activateTab(this.opts.action); - this.initAffix(); - } - - MergeRequestTabs.prototype.bindEvents = function() { - $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); - $(document).on('click', '.js-show-tab', this.showTab); - }; - - MergeRequestTabs.prototype.unbindEvents = function() { - $(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); - $(document).off('click', '.js-show-tab', this.showTab); - }; - - MergeRequestTabs.prototype.showTab = function(event) { - event.preventDefault(); - return this.activateTab($(event.target).data('action')); - }; - - MergeRequestTabs.prototype.tabShown = function(event) { - var $target, action, navBarHeight; - $target = $(event.target); - action = $target.data('action'); - if (action === 'commits') { - this.loadCommits($target.attr('href')); - this.expandView(); - this.resetViewContainer(); - } else if (this.isDiffAction(action)) { - this.loadDiff($target.attr('href')); - if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { - this.shrinkView(); - } - if (this.diffViewType() === 'parallel') { - this.expandViewContainer(); - } - navBarHeight = $('.navbar-gitlab').outerHeight(); - $.scrollTo(".merge-request-details .merge-request-tabs", { - offset: -navBarHeight - }); - } else if (action === 'builds') { - this.loadBuilds($target.attr('href')); - this.expandView(); - this.resetViewContainer(); - } else if (action === 'pipelines') { - this.loadPipelines($target.attr('href')); - this.expandView(); - this.resetViewContainer(); - } else { - this.expandView(); - this.resetViewContainer(); - } - if (this.opts.setUrl) { - this.setCurrentAction(action); - } - }; - - MergeRequestTabs.prototype.scrollToElement = function(container) { - var $el, navBarHeight; - if (window.location.hash) { - navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + document.querySelector('.js-tabs-affix').offsetHeight; - $el = $(container + " " + window.location.hash + ":not(.match)"); - if ($el.length) { - return $.scrollTo(container + " " + window.location.hash + ":not(.match)", { - offset: -navBarHeight - }); - } - } - }; - - // Activate a tab based on the current action - MergeRequestTabs.prototype.activateTab = function(action) { - if (action === 'show') { - action = 'notes'; - } - // important note: the .tab('show') method triggers 'shown.bs.tab' event itself - $(".merge-request-tabs a[data-action='" + action + "']").tab('show'); - }; - - // Replaces the current Merge Request-specific action in the URL with a new one - // - // If the action is "notes", the URL is reset to the standard - // `MergeRequests#show` route. - // - // Examples: - // - // location.pathname # => "/namespace/project/merge_requests/1" - // setCurrentAction('diffs') - // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // - // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // setCurrentAction('notes') - // location.pathname # => "/namespace/project/merge_requests/1" - // - // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // setCurrentAction('commits') - // location.pathname # => "/namespace/project/merge_requests/1/commits" - // - // Returns the new URL String - MergeRequestTabs.prototype.setCurrentAction = function(action) { - var new_state; - // Normalize action, just to be safe - if (action === 'show') { - action = 'notes'; - } - this.currentAction = action; - // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs' - new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); - - // Append the new action if we're on a tab other than 'notes' - if (action !== 'notes') { - new_state += "/" + action; - } - // Ensure parameters and hash come along for the ride - new_state += this._location.search + this._location.hash; - history.replaceState({ - turbolinks: true, - url: new_state - // Replace the current history state with the new one without breaking - // Turbolinks' history. - // - // See https://github.com/rails/turbolinks/issues/363 - }, document.title, new_state); - return new_state; - }; - - MergeRequestTabs.prototype.loadCommits = function(source) { - if (this.commitsLoaded) { - return; - } - return this._get({ - url: source + ".json", - success: (function(_this) { - return function(data) { - document.querySelector("div#commits").innerHTML = data.html; - gl.utils.localTimeAgo($('.js-timeago', 'div#commits')); - _this.commitsLoaded = true; - return _this.scrollToElement("#commits"); - }; - })(this) - }); - }; - - MergeRequestTabs.prototype.loadDiff = function(source) { - if (this.diffsLoaded) { - return; - } - - // We extract pathname for the current Changes tab anchor href - // some pages like MergeRequestsController#new has query parameters on that anchor - var url = document.createElement('a'); - url.href = source; - - return this._get({ - url: (url.pathname + ".json") + this._location.search, - success: (function(_this) { - return function(data) { - $('#diffs').html(data.html); - - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - gl.diffNotesCompileComponents(); - } - - gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); - $('#diffs .js-syntax-highlight').syntaxHighlight(); - $('#diffs .diff-file').singleFileDiff(); - if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) { - _this.expandViewContainer(); - } - _this.diffsLoaded = true; - var anchoredDiff = gl.utils.getLocationHash(); - if (anchoredDiff) _this.openAnchoredDiff(anchoredDiff, function() { - _this.scrollToElement("#diffs"); - _this.highlighSelectedLine(); - }); - _this.filesCommentButton = $('.files .diff-file').filesCommentButton(); - return $(document).off('click', '.diff-line-num a').on('click', '.diff-line-num a', function(e) { - e.preventDefault(); - window.location.hash = $(e.currentTarget).attr('href'); - _this.highlighSelectedLine(); - return _this.scrollToElement("#diffs"); - }); - }; - })(this) - }); - }; - - MergeRequestTabs.prototype.openAnchoredDiff = function(anchoredDiff, cb) { - var diffTitle = $('#file-path-' + anchoredDiff); - var diffFile = diffTitle.closest('.diff-file'); - var nothingHereBlock = $('.nothing-here-block:visible', diffFile); - if (nothingHereBlock.length) { - diffFile.singleFileDiff(true, cb); - } else { - cb(); - } - }; - - MergeRequestTabs.prototype.highlighSelectedLine = function() { - var $diffLine, diffLineTop, hashClassString, locationHash, navBarHeight; - $('.hll').removeClass('hll'); - locationHash = window.location.hash; - if (locationHash !== '') { - dataLineString = '[data-line-code="' + locationHash.replace('#', '') + '"]'; - $diffLine = $(locationHash + ":not(.match)", $('#diffs')); - if (!$diffLine.is('tr')) { - $diffLine = $('#diffs').find("td" + locationHash + ", td" + dataLineString); - } else { - $diffLine = $diffLine.find('td'); - } - if ($diffLine.length) { - $diffLine.addClass('hll'); - diffLineTop = $diffLine.offset().top; - return navBarHeight = $('.navbar-gitlab').outerHeight(); - } - } - }; - - MergeRequestTabs.prototype.loadBuilds = function(source) { - if (this.buildsLoaded) { - return; - } - return this._get({ - url: source + ".json", - success: (function(_this) { - return function(data) { - document.querySelector("div#builds").innerHTML = data.html; - gl.utils.localTimeAgo($('.js-timeago', 'div#builds')); - _this.buildsLoaded = true; - if (!this.pipelines) this.pipelines = new gl.Pipelines(); - return _this.scrollToElement("#builds"); - }; - })(this) - }); - }; - - MergeRequestTabs.prototype.loadPipelines = function(source) { - if (this.pipelinesLoaded) { - return; - } - return this._get({ - url: source + ".json", - success: function(data) { - $('#pipelines').html(data.html); - gl.utils.localTimeAgo($('.js-timeago', '#pipelines')); - this.pipelinesLoaded = true; - return this.scrollToElement("#pipelines"); - }.bind(this) - }); - }; - - // Show or hide the loading spinner - // - // status - Boolean, true to show, false to hide - MergeRequestTabs.prototype.toggleLoading = function(status) { - return $('.mr-loading-status .loading').toggle(status); - }; - - MergeRequestTabs.prototype._get = function(options) { - var defaults; - defaults = { - beforeSend: (function(_this) { - return function() { - return _this.toggleLoading(true); - }; - })(this), - complete: (function(_this) { - return function() { - return _this.toggleLoading(false); - }; - })(this), - dataType: 'json', - type: 'GET' - }; - options = $.extend({}, defaults, options); - return $.ajax(options); - }; - - MergeRequestTabs.prototype.diffViewType = function() { - return $('.inline-parallel-buttons a.active').data('view-type'); - }; - - MergeRequestTabs.prototype.isDiffAction = function(action) { - return action === 'diffs' || action === 'new/diffs' - }; - - MergeRequestTabs.prototype.expandViewContainer = function() { - var $wrapper = $('.content-wrapper .container-fluid'); - if (this.fixedLayoutPref === null) { - this.fixedLayoutPref = $wrapper.hasClass('container-limited'); - } - $wrapper.removeClass('container-limited'); - }; - - MergeRequestTabs.prototype.resetViewContainer = function() { - if (this.fixedLayoutPref !== null) { - $('.content-wrapper .container-fluid') - .toggleClass('container-limited', this.fixedLayoutPref); - } - }; - - MergeRequestTabs.prototype.shrinkView = function() { - var $gutterIcon; - $gutterIcon = $('.js-sidebar-toggle i:visible'); - return setTimeout(function() { - if ($gutterIcon.is('.fa-angle-double-right')) { - return $gutterIcon.closest('a').trigger('click', [true]); - } - // Wait until listeners are set - // Only when sidebar is expanded - }, 0); - }; - - MergeRequestTabs.prototype.expandView = function() { - var $gutterIcon; - if (Cookies.get('collapsed_gutter') === 'true') { - return; - } - $gutterIcon = $('.js-sidebar-toggle i:visible'); - return setTimeout(function() { - if ($gutterIcon.is('.fa-angle-double-left')) { - return $gutterIcon.closest('a').trigger('click', [true]); - } - }, 0); - // Expand the issuable sidebar unless the user explicitly collapsed it - // Wait until listeners are set - // Only when sidebar is collapsed - }; - - MergeRequestTabs.prototype.initAffix = function () { - var $tabs = $('.js-tabs-affix'); - - // Screen space on small screens is usually very sparse - // So we dont affix the tabs on these - if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; - - var $diffTabs = $('#diff-notes-app'), - $fixedNav = $('.navbar-fixed-top'), - $layoutNav = $('.layout-nav'); - - $tabs.off('affix.bs.affix affix-top.bs.affix') - .affix({ - offset: { - top: function () { - var tabsTop = $diffTabs.offset().top - $tabs.height(); - tabsTop = tabsTop - ($fixedNav.height() + $layoutNav.height()); - - return tabsTop; - } - } - }).on('affix.bs.affix', function () { - $diffTabs.css({ - marginTop: $tabs.height() - }); - }).on('affix-top.bs.affix', function () { - $diffTabs.css({ - marginTop: '' - }); - }); - - // Fix bug when reloading the page already scrolling - if ($tabs.hasClass('affix')) { - $tabs.trigger('affix.bs.affix'); - } - }; - - return MergeRequestTabs; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 new file mode 100644 index 00000000000..3ec0f1fd613 --- /dev/null +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -0,0 +1,389 @@ +/* eslint-disable no-new, class-methods-use-this */ +/* global Breakpoints */ +/* global Cookies */ +/* global DiffNotesApp */ +/* global Flash */ + +/*= require js.cookie */ +/*= require breakpoints */ + +/* eslint-disable max-len */ +// MergeRequestTabs +// +// Handles persisting and restoring the current tab selection and lazily-loading +// content on the MergeRequests#show page. +// +// ### Example Markup +// +// <ul class="nav-links merge-request-tabs"> +// <li class="notes-tab active"> +// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1"> +// Discussion +// </a> +// </li> +// <li class="commits-tab"> +// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits"> +// Commits +// </a> +// </li> +// <li class="diffs-tab"> +// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs"> +// Diffs +// </a> +// </li> +// </ul> +// +// <div class="tab-content"> +// <div class="notes tab-pane active" id="notes"> +// Notes Content +// </div> +// <div class="commits tab-pane" id="commits"> +// Commits Content +// </div> +// <div class="diffs tab-pane" id="diffs"> +// Diffs Content +// </div> +// </div> +// +// <div class="mr-loading-status"> +// <div class="loading"> +// Loading Animation +// </div> +// </div> +// +/* eslint-enable max-len */ + +(() => { + // Store the `location` object, allowing for easier stubbing in tests + let location = window.location; + + class MergeRequestTabs { + + constructor({ action, setUrl, buildsLoaded, stubLocation } = {}) { + this.diffsLoaded = false; + this.buildsLoaded = false; + this.pipelinesLoaded = false; + this.commitsLoaded = false; + this.fixedLayoutPref = null; + + this.setUrl = setUrl !== undefined ? setUrl : true; + this.buildsLoaded = buildsLoaded || false; + + this.setCurrentAction = this.setCurrentAction.bind(this); + this.tabShown = this.tabShown.bind(this); + this.showTab = this.showTab.bind(this); + + if (stubLocation) { + location = stubLocation; + } + + this.bindEvents(); + this.activateTab(action); + this.initAffix(); + } + + bindEvents() { + $(document) + .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) + .on('click', '.js-show-tab', this.showTab); + } + + unbindEvents() { + $(document) + .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) + .off('click', '.js-show-tab', this.showTab); + } + + showTab(e) { + e.preventDefault(); + this.activateTab($(e.target).data('action')); + } + + tabShown(e) { + const $target = $(e.target); + const action = $target.data('action'); + + if (action === 'commits') { + this.loadCommits($target.attr('href')); + this.expandView(); + this.resetViewContainer(); + } else if (this.isDiffAction(action)) { + this.loadDiff($target.attr('href')); + if (Breakpoints.get().getBreakpointSize() !== 'lg') { + this.shrinkView(); + } + if (this.diffViewType() === 'parallel') { + this.expandViewContainer(); + } + const navBarHeight = $('.navbar-gitlab').outerHeight(); + $.scrollTo('.merge-request-details .merge-request-tabs', { + offset: -navBarHeight, + }); + } else if (action === 'builds') { + this.loadBuilds($target.attr('href')); + this.expandView(); + this.resetViewContainer(); + } else if (action === 'pipelines') { + this.loadPipelines($target.attr('href')); + this.expandView(); + this.resetViewContainer(); + } else { + this.expandView(); + this.resetViewContainer(); + } + if (this.setUrl) { + this.setCurrentAction(action); + } + } + + scrollToElement(container) { + if (location.hash) { + const offset = 0 - ( + $('.navbar-gitlab').outerHeight() + + $('.layout-nav').outerHeight() + + $('.js-tabs-affix').outerHeight() + ); + const $el = $(`${container} ${location.hash}:not(.match)`); + if ($el.length) { + $.scrollTo($el[0], { offset }); + } + } + } + + // Activate a tab based on the current action + activateTab(action) { + const activate = action === 'show' ? 'notes' : action; + // important note: the .tab('show') method triggers 'shown.bs.tab' event itself + $(`.merge-request-tabs a[data-action='${activate}']`).tab('show'); + } + + // Replaces the current Merge Request-specific action in the URL with a new one + // + // If the action is "notes", the URL is reset to the standard + // `MergeRequests#show` route. + // + // Examples: + // + // location.pathname # => "/namespace/project/merge_requests/1" + // setCurrentAction('diffs') + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // setCurrentAction('notes') + // location.pathname # => "/namespace/project/merge_requests/1" + // + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // setCurrentAction('commits') + // location.pathname # => "/namespace/project/merge_requests/1/commits" + // + // Returns the new URL String + setCurrentAction(action) { + this.currentAction = action === 'show' ? 'notes' : action; + + // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs' + let newState = location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); + + // Append the new action if we're on a tab other than 'notes' + if (this.currentAction !== 'notes') { + newState += `/${this.currentAction}`; + } + + // Ensure parameters and hash come along for the ride + newState += location.search + location.hash; + + // Replace the current history state with the new one without breaking + // Turbolinks' history. + // + // See https://github.com/rails/turbolinks/issues/363 + window.history.replaceState({ + turbolinks: true, + url: newState, + }, document.title, newState); + + return newState; + } + + loadCommits(source) { + if (this.commitsLoaded) { + return; + } + this.ajaxGet({ + url: `${source}.json`, + success: (data) => { + document.querySelector('div#commits').innerHTML = data.html; + gl.utils.localTimeAgo($('.js-timeago', 'div#commits')); + this.commitsLoaded = true; + this.scrollToElement('#commits'); + }, + }); + } + + loadDiff(source) { + if (this.diffsLoaded) { + return; + } + + // We extract pathname for the current Changes tab anchor href + // some pages like MergeRequestsController#new has query parameters on that anchor + const urlPathname = gl.utils.parseUrlPathname(source); + + this.ajaxGet({ + url: `${urlPathname}.json${location.search}`, + success: (data) => { + $('#diffs').html(data.html); + + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + gl.diffNotesCompileComponents(); + } + + gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); + $('#diffs .js-syntax-highlight').syntaxHighlight(); + + if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) { + this.expandViewContainer(); + } + this.diffsLoaded = true; + + const diffPage = new gl.Diff(); + + const locationHash = gl.utils.getLocationHash(); + const anchoredDiff = locationHash && locationHash.split('_')[0]; + if (anchoredDiff) { + diffPage.openAnchoredDiff(anchoredDiff, () => this.scrollToElement('#diffs')); + } + }, + }); + } + + loadBuilds(source) { + if (this.buildsLoaded) { + return; + } + this.ajaxGet({ + url: `${source}.json`, + success: (data) => { + document.querySelector('div#builds').innerHTML = data.html; + gl.utils.localTimeAgo($('.js-timeago', 'div#builds')); + this.buildsLoaded = true; + new gl.Pipelines(); + this.scrollToElement('#builds'); + }, + }); + } + + loadPipelines(source) { + if (this.pipelinesLoaded) { + return; + } + this.ajaxGet({ + url: `${source}.json`, + success: (data) => { + $('#pipelines').html(data.html); + gl.utils.localTimeAgo($('.js-timeago', '#pipelines')); + this.pipelinesLoaded = true; + this.scrollToElement('#pipelines'); + }, + }); + } + + // Show or hide the loading spinner + // + // status - Boolean, true to show, false to hide + toggleLoading(status) { + $('.mr-loading-status .loading').toggle(status); + } + + ajaxGet(options) { + const defaults = { + beforeSend: () => this.toggleLoading(true), + error: () => new Flash('An error occurred while fetching this tab.', 'alert'), + complete: () => this.toggleLoading(false), + dataType: 'json', + type: 'GET', + }; + $.ajax($.extend({}, defaults, options)); + } + + diffViewType() { + return $('.inline-parallel-buttons a.active').data('view-type'); + } + + isDiffAction(action) { + return action === 'diffs' || action === 'new/diffs'; + } + + expandViewContainer() { + const $wrapper = $('.content-wrapper .container-fluid'); + if (this.fixedLayoutPref === null) { + this.fixedLayoutPref = $wrapper.hasClass('container-limited'); + } + $wrapper.removeClass('container-limited'); + } + + resetViewContainer() { + if (this.fixedLayoutPref !== null) { + $('.content-wrapper .container-fluid') + .toggleClass('container-limited', this.fixedLayoutPref); + } + } + + shrinkView() { + const $gutterIcon = $('.js-sidebar-toggle i:visible'); + + // Wait until listeners are set + setTimeout(() => { + // Only when sidebar is expanded + if ($gutterIcon.is('.fa-angle-double-right')) { + $gutterIcon.closest('a').trigger('click', [true]); + } + }, 0); + } + + // Expand the issuable sidebar unless the user explicitly collapsed it + expandView() { + if (Cookies.get('collapsed_gutter') === 'true') { + return; + } + const $gutterIcon = $('.js-sidebar-toggle i:visible'); + + // Wait until listeners are set + setTimeout(() => { + // Only when sidebar is collapsed + if ($gutterIcon.is('.fa-angle-double-left')) { + $gutterIcon.closest('a').trigger('click', [true]); + } + }, 0); + } + + initAffix() { + const $tabs = $('.js-tabs-affix'); + + // Screen space on small screens is usually very sparse + // So we dont affix the tabs on these + if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; + + const $diffTabs = $('#diff-notes-app'); + const $fixedNav = $('.navbar-fixed-top'); + const $layoutNav = $('.layout-nav'); + + $tabs.off('affix.bs.affix affix-top.bs.affix') + .affix({ + offset: { + top: () => ( + $diffTabs.offset().top - $tabs.height() - $fixedNav.height() - $layoutNav.height() + ), + }, + }) + .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() })) + .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' })); + + // Fix bug when reloading the page already scrolling + if ($tabs.hasClass('affix')) { + $tabs.trigger('affix.bs.affix'); + } + } + } + + window.gl = window.gl || {}; + window.gl.MergeRequestTabs = MergeRequestTabs; +})(); diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 2767849e673..0d48e69cce9 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -14,6 +14,7 @@ COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>'; function SingleFileDiff(file, forceLoad, cb) { + var clickTarget; this.file = file; this.toggleDiff = bind(this.toggleDiff, this); this.content = $('.diff-content', this.file); @@ -31,9 +32,9 @@ this.content.after(this.collapsedContent); this.$toggleIcon.addClass('fa-caret-down'); } - $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff); + clickTarget = $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff); if (forceLoad) { - this.toggleDiff(null, cb); + this.toggleDiff({ target: clickTarget }, cb); } } diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 4aaff7d04f1..4d4835568ed 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -40,3 +40,6 @@ @import "framework/blank"; @import "framework/wells.scss"; @import "framework/page-header.scss"; +@import "framework/awards.scss"; +@import "framework/images.scss"; +@import "framework/broadcast-messages"; diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/framework/awards.scss index dce5c31f282..c13cb4a02b2 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -127,7 +127,7 @@ .award-control-icon { float: left; margin-right: 5px; - font-size: 19px; + font-size: 18px; } .award-control-icon-loading { diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss index 540718197e0..a2fa2e7769b 100644 --- a/app/assets/stylesheets/framework/blank.scss +++ b/app/assets/stylesheets/framework/blank.scss @@ -32,14 +32,14 @@ .blank-state-title { margin-top: 0; margin-bottom: 5px; - font-size: 19px; + font-size: 18px; font-weight: normal; } .blank-state-text { margin-top: 0; margin-bottom: $gl-padding; - font-size: 15px; + font-size: 14px; > strong { font-weight: 600; diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 57db5eaa2b3..95c02499271 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -1,8 +1,3 @@ -.light-well { - background-color: $background-color; - padding: 15px; -} - .centered-light-block { text-align: center; color: $gl-gray; @@ -274,6 +269,10 @@ } } + .emoji-icon { + display: inline-block; + } + @media(max-width: $screen-xs-max) { margin-top: 50px; text-align: center; diff --git a/app/assets/stylesheets/framework/broadcast-messages.scss b/app/assets/stylesheets/framework/broadcast-messages.scss new file mode 100644 index 00000000000..9b54fb94cdc --- /dev/null +++ b/app/assets/stylesheets/framework/broadcast-messages.scss @@ -0,0 +1,21 @@ +.broadcast-message { + @extend .alert-warning; + padding: 10px; + text-align: center; + + div, + p { + display: inline; + margin: 0; + + a { + color: inherit; + text-decoration: underline; + } + } +} + +.broadcast-message-preview { + @extend .broadcast-message; + margin-bottom: 20px; +} diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index cdeef6fcc9e..600bf17259b 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -33,7 +33,7 @@ .slead { color: $common-gray; - font-size: 15px; + font-size: 14px; margin-bottom: 12px; font-weight: normal; line-height: 24px; @@ -379,7 +379,9 @@ table { border-top: 1px solid $border-color; } -.hide-bottom-border { border-bottom: none !important; } +.hide-bottom-border { + border-bottom: none !important; +} .gl-accessibility { &:focus { @@ -396,3 +398,13 @@ table { z-index: 1; } } + +.str-truncated { + &-60 { + @include str-truncated(60%); + } + + &-100 { + @include str-truncated(100%); + } +} diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index e6229a35b88..33de652c06f 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -36,7 +36,7 @@ padding: 6px 8px 6px 10px; background-color: $dropdown-toggle-bg; color: $dropdown-toggle-color; - font-size: 15px; + font-size: 14px; text-align: left; border: 1px solid $border-color; border-radius: $border-radius-base; @@ -123,7 +123,7 @@ width: 240px; margin-top: 2px; margin-bottom: 0; - font-size: 15px; + font-size: 14px; font-weight: normal; padding: 8px 0; background-color: $dropdown-bg; @@ -589,7 +589,7 @@ .ui-datepicker-title { color: $gl-gray; - font-size: 15px; + font-size: 14px; line-height: 1; font-weight: normal; } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index e40ff4d4688..cc2286038c0 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -71,7 +71,7 @@ header { } .fa-caret-down { - font-size: 15px; + font-size: 14px; } } @@ -156,7 +156,7 @@ header { position: relative; padding-right: 20px; margin: 0; - font-size: 19px; + font-size: 18px; max-width: 385px; display: inline-block; line-height: $header-height; diff --git a/app/assets/stylesheets/pages/appearances.scss b/app/assets/stylesheets/framework/images.scss index 878f44116ba..878f44116ba 100644 --- a/app/assets/stylesheets/pages/appearances.scss +++ b/app/assets/stylesheets/framework/images.scss diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index db8677433bb..ed4b60faf92 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -106,13 +106,13 @@ ul.task-list { } } +// Generic content list ul.content-list { @include basic-list; - margin: 0; padding: 0; - > li { + li { border-color: $table-border-color; font-size: $list-font-size; color: $list-text-color; @@ -193,6 +193,41 @@ ul.content-list { } } +// Content list using flexbox +.flex-list { + .flex-row { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + white-space: nowrap; + } + + .row-main-content { + flex: 1 1 auto; + overflow: hidden; + padding-right: 8px; + } + + .row-title { + font-weight: 600; + } + + .row-second-line { + display: block; + } + + .dropdown { + .btn-block { + margin-bottom: 0; + line-height: inherit; + } + } + + .label-default { + color: $btn-transparent-color; + } +} + .panel > .content-list > li { padding: $gl-padding-top $gl-padding; diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index d2d3fc23b6c..69da520f21f 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -49,7 +49,7 @@ padding: $gl-btn-padding; padding-bottom: 11px; margin-bottom: -1px; - font-size: 15px; + font-size: 14px; line-height: 28px; color: $note-toolbar-color; border-bottom: 2px solid transparent; @@ -268,6 +268,16 @@ width: auto; } } + + &.multi-line { + .nav-text { + line-height: 20px; + } + + .nav-controls { + padding: 17px 0; + } + } } .layout-nav { diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index a5f36c177fc..5d0ca63ea08 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -34,6 +34,10 @@ table { background-color: $background-color; font-weight: normal; border-bottom: none; + + &.wide { + width: 55%; + } } td { @@ -42,3 +46,16 @@ table { } } } + +.responsive-table { + @media (max-width: $screen-sm-max) { + th { + width: 100%; + } + + td { + width: 100%; + float: left; + } + } +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index edfb2c33f4a..18716813c48 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -102,7 +102,7 @@ $well-light-text-color: #5b6169; /* * Text */ -$gl-font-size: 15px; +$gl-font-size: 14px; $gl-title-color: #333; $gl-text-color: #5c5c5c; $gl-text-color-dark: #5c5d5e; @@ -380,7 +380,7 @@ $ci-skipped-color: #888; /* * Boards */ -$issue-boards-font-size: 15px; +$issue-boards-font-size: 14px; $issue-boards-card-shadow: rgba(186, 186, 186, 0.5); /* @@ -427,12 +427,6 @@ $common-gray-dark: #444; $common-red: $gl-text-red; $common-green: $gl-text-green; - -/* -* Dashboard -*/ -$dashboard-project-access-icon-color: #888; - /* * Editor */ diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss index 192939f4527..f2860dfe84d 100644 --- a/app/assets/stylesheets/framework/wells.scss +++ b/app/assets/stylesheets/framework/wells.scss @@ -43,3 +43,16 @@ background-color: $well-expand-item; } } + +.light-well { + background-color: $background-color; + padding: 15px; +} + +.well-centered { + h1 { + font-weight: normal; + text-align: center; + font-size: 48px; + } +} diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss deleted file mode 100644 index 291372b88e3..00000000000 --- a/app/assets/stylesheets/pages/admin.scss +++ /dev/null @@ -1,168 +0,0 @@ -/** - * Admin area - * - */ -.admin-dashboard { - .data { - a { - h1 { - line-height: 48px; - font-size: 48px; - padding: 20px; - text-align: center; - font-weight: normal; - } - } - } - - .str-truncated { - max-width: 60%; - } -} - -.admin-filter form { - .select2-container { - width: 100%; - } - - .controls { - margin-left: 130px; - } - - .form-actions { - padding-left: 130px; - background: $white-light; - } - - .visibility-levels { - .controls { - margin-bottom: 9px; - } - - i { - color: inherit; - } - } -} - -.broadcast-messages { - .message { - line-height: 2; - } -} - -.broadcast-message { - @extend .alert-warning; - padding: 10px; - text-align: center; - - > div, - p { - display: inline; - margin: 0; - - a { - color: inherit; - text-decoration: underline; - } - } -} - -.broadcast-message-preview { - @extend .broadcast-message; - margin-bottom: 20px; -} - -// Users List - -.users-list { - .user-row { - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - white-space: nowrap; - } - - .user-details { - flex: 1 1 auto; - overflow: hidden; - padding-right: 8px; - } - - .user-name { - display: inline-block; - font-weight: 600; - } - - .user-name, - .user-email { - overflow: hidden; - text-overflow: ellipsis; - } - - .dropdown { - .btn-block { - margin-bottom: 0; - line-height: inherit; - } - } - - .label-default { - color: $btn-transparent-color; - } -} - -.abuse-reports { - .table { - table-layout: fixed; - } - - .subheading { - padding-bottom: $gl-padding; - } - - .message { - word-wrap: break-word; - } - - .btn { - white-space: normal; - padding: $gl-btn-padding; - } - - th { - width: 15%; - - &.wide { - width: 55%; - } - } - - @media (max-width: $screen-sm-max) { - th { - width: 100%; - } - - td { - width: 100%; - float: left; - } - } - - .no-reports { - .emoji-icon { - margin-left: $btn-side-margin; - margin-top: 3px; - } - - span { - font-size: 19px; - } - } -} - -.admin-builds-table { - .ci-table td:last-child { - min-width: 120px; - } -} diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 842c0434bf2..dcc13f6d74a 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -64,6 +64,7 @@ @media (max-width: $screen-sm-max) { padding-right: 40px; + margin-top: 6px; .btn-inverted { display: none; diff --git a/app/assets/stylesheets/pages/confirmation.scss b/app/assets/stylesheets/pages/confirmation.scss deleted file mode 100644 index 8aab5e8231d..00000000000 --- a/app/assets/stylesheets/pages/confirmation.scss +++ /dev/null @@ -1,32 +0,0 @@ -.well-confirmation { - margin-bottom: 20px; - border-bottom: 1px solid $gray-darker; - - > h1, - h2, - h3, - h4, - h5, - h6 { - font-weight: 400; - } - - .lead { - margin-bottom: 20px; - } - - ul, - ol { - padding-left: 0; - } - - li { - list-style-type: none; - } -} - -.confirmation-content { - a { - color: $md-link-color; - } -} diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index e7a2c91003f..57146e1fccd 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -109,7 +109,7 @@ &.title { line-height: 19px; - font-size: 15px; + font-size: 14px; font-weight: 600; color: $gl-title-color; } diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss deleted file mode 100644 index 4421ed6a0b9..00000000000 --- a/app/assets/stylesheets/pages/dashboard.scss +++ /dev/null @@ -1,43 +0,0 @@ -.dashboard { - .side { - .panel { - .panel-heading { - background: $background-color; - border-top-left-radius: 0; - } - - border-top-left-radius: 0; - } - } -} - -.dashboard-search-filter { - padding: 5px; - - .search-text-input { - float: left; - @extend .col-md-2; - } - - .btn { - margin-left: 5px; - float: left; - } -} - -.project-access-icon { - margin-left: 10px; - float: left; - margin-right: 15px; - margin-bottom: 15px; - - i { - color: $dashboard-project-access-icon-color; - } -} - -.dash-project-access-icon { - float: left; - margin-right: 5px; - width: 16px; -} diff --git a/app/assets/stylesheets/pages/errors.scss b/app/assets/stylesheets/pages/errors.scss deleted file mode 100644 index 11309817d31..00000000000 --- a/app/assets/stylesheets/pages/errors.scss +++ /dev/null @@ -1,16 +0,0 @@ -.error-page { - max-width: 400px; - margin: 0 auto; - - h1, - h2, - h3 { - text-align: center; - } - - h1 { - font-size: 56px; - line-height: 100px; - font-weight: 300; - } -} diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss index 8d30bd64278..a7c80dce424 100644 --- a/app/assets/stylesheets/pages/lint.scss +++ b/app/assets/stylesheets/pages/lint.scss @@ -1,11 +1,11 @@ .ci-body { .incorrect-syntax { - font-size: 19px; + font-size: 18px; color: $lint-incorrect-color; } .correct-syntax { - font-size: 19px; + font-size: 18px; color: $lint-correct-color; } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 1c6fe7afe14..6234779ac19 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -116,7 +116,7 @@ @media (max-width: $screen-xs-max) { h4 { - font-size: 15px; + font-size: 14px; } p { @@ -129,6 +129,11 @@ margin-bottom: 4px; } + .btn-grouped { + float: none; + margin-right: 0; + } + .accept-action { width: 100%; text-align: center; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index ff092d53845..c35d71f9e7b 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -129,7 +129,7 @@ .note-edit-form { display: none; - font-size: 15px; + font-size: 14px; .md-area { background-color: $white-light; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 0027d2caf22..08062b85504 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -280,6 +280,12 @@ } } +.admin-builds-table { + .ci-table td:last-child { + min-width: 120px; + } +} + // Pipeline visualization .toggle-pipeline-btn { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 62862c72b3b..72b6685d940 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -521,7 +521,7 @@ a.deploy-project-label { .nav > li > a { padding: 0; background-color: transparent; - font-size: 15px; + font-size: 14px; line-height: 29px; color: $notes-light-color; diff --git a/app/assets/stylesheets/pages/tags.scss b/app/assets/stylesheets/pages/tags.scss deleted file mode 100644 index 24ebd3e7cfa..00000000000 --- a/app/assets/stylesheets/pages/tags.scss +++ /dev/null @@ -1,7 +0,0 @@ -.tag-buttons { - line-height: 40px; - - .btn:not(.dropdown-toggle) { - margin-left: 10px; - } -} diff --git a/app/assets/stylesheets/pages/votes.scss b/app/assets/stylesheets/pages/votes.scss deleted file mode 100644 index dc9a7d71e8b..00000000000 --- a/app/assets/stylesheets/pages/votes.scss +++ /dev/null @@ -1,4 +0,0 @@ -.votes-inline { - display: inline-block; - margin: 0 8px; -} diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb index 6546a07b41c..fdb05bb3228 100644 --- a/app/controllers/concerns/merge_requests_action.rb +++ b/app/controllers/concerns/merge_requests_action.rb @@ -6,7 +6,12 @@ module MergeRequestsAction @label = merge_requests_finder.labels.first @merge_requests = merge_requests_collection - .non_archived .page(params[:page]) end + + private + + def filter_params + super.merge(non_archived: true) + end end diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb index d174e1145a7..148e39630e3 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -5,9 +5,7 @@ class Projects::DiscussionsController < Projects::ApplicationController before_action :authorize_resolve_discussion! def resolve - discussion.resolve!(current_user) - - MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request) + Discussions::ResolveService.new(project, current_user, merge_request: merge_request).execute(discussion) render json: { resolved_by: discussion.resolved_by.try(:name), diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 4aea7bb62c4..4f66e01e0f7 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -46,8 +46,9 @@ class Projects::IssuesController < Projects::ApplicationController params[:issue] ||= ActionController::Parameters.new( assignee_id: "" ) + build_params = issue_params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions) + @issue = @noteable = Issues::BuildService.new(project, current_user, build_params).execute - @issue = @noteable = @project.issues.new(issue_params) respond_with(@issue) end @@ -75,7 +76,9 @@ class Projects::IssuesController < Projects::ApplicationController end def create - @issue = Issues::CreateService.new(project, current_user, issue_params.merge(request: request)).execute + extra_params = { request: request, + merge_request_for_resolving_discussions: merge_request_for_resolving_discussions } + @issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute respond_to do |format| format.html do @@ -169,6 +172,14 @@ class Projects::IssuesController < Projects::ApplicationController alias_method :awardable, :issue alias_method :spammable, :issue + def merge_request_for_resolving_discussions + return unless merge_request_iid = params[:merge_request_for_resolving_discussions] + + @merge_request_for_resolving_discussions ||= MergeRequestsFinder.new(current_user, project_id: project.id). + execute. + find_by(iid: merge_request_iid) + end + def authorize_read_issue! return render_404 unless can?(current_user, :read_issue, @issue) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index d2cef52842c..f0cb5a9d4b4 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -302,9 +302,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def cancel_merge_when_build_succeeds - return access_denied! unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user) + unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user) + return access_denied! + end - MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user).cancel(@merge_request) + MergeRequests::MergeWhenPipelineSucceedsService + .new(@project, current_user) + .cancel(@merge_request) end def merge @@ -331,8 +335,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController end if @merge_request.head_pipeline.active? - MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) - .execute(@merge_request) + MergeRequests::MergeWhenPipelineSucceedsService + .new(@project, current_user, merge_params) + .execute(@merge_request) + @status = :merge_when_build_succeeds elsif @merge_request.head_pipeline.success? # This can be triggered when a user clicks the auto merge button while diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 001c83ccb4b..c9bee01b9ad 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -7,7 +7,7 @@ # current_user - which user use # params: # scope: 'created-by-me' or 'assigned-to-me' or 'all' -# state: 'open' or 'closed' or 'all' +# state: 'opened' or 'closed' or 'all' # group_id: integer # project_id: integer # milestone_title: string @@ -15,6 +15,7 @@ # search: string # label_name: string # sort: string +# non_archived: boolean # class IssuableFinder NONE = '0' @@ -38,6 +39,7 @@ class IssuableFinder items = by_author(items) items = by_label(items) items = by_due_date(items) + items = by_non_archived(items) sort(items) end @@ -207,10 +209,13 @@ class IssuableFinder end def by_state(items) - params[:state] ||= 'all' - - if items.respond_to?(params[:state]) - items.public_send(params[:state]) + case params[:state].to_s + when 'closed' + items.closed + when 'merged' + items.respond_to?(:merged) ? items.merged : items.closed + when 'opened' + items.opened else items end @@ -353,6 +358,10 @@ class IssuableFinder end end + def by_non_archived(items) + params[:non_archived].present? ? items.non_archived : items + end + def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' end diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 3b254e7d9d5..8b82255445e 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -14,6 +14,7 @@ # search: string # label_name: string # sort: string +# non_archived: boolean # class MergeRequestsFinder < IssuableFinder def klass diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index abcf84b4d15..8e19752a8a1 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -5,8 +5,9 @@ module CiStatusHelper end def ci_status_with_icon(status, target = nil) - content = ci_icon_for_status(status) + ci_label_for_status(status) + content = ci_icon_for_status(status) + ci_text_for_status(status) klass = "ci-status ci-#{status}" + if target link_to content, target, class: klass else @@ -14,7 +15,19 @@ module CiStatusHelper end end + def ci_text_for_status(status) + if detailed_status?(status) + status.text + else + status + end + end + def ci_label_for_status(status) + if detailed_status?(status) + return status.label + end + case status when 'success' 'passed' @@ -31,6 +44,10 @@ module CiStatusHelper end def ci_icon_for_status(status) + if detailed_status?(status) + return custom_icon(status.icon) + end + icon_name = case status when 'success' @@ -94,4 +111,10 @@ module CiStatusHelper class: klass, title: title, data: data end end + + def detailed_status?(status) + status.respond_to?(:text) && + status.respond_to?(:label) && + status.respond_to?(:icon) + end end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 0772d848289..eb435cc1783 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -174,7 +174,7 @@ module GitlabMarkdownHelper # Returns a String def cross_project_reference(project, entity) if entity.respond_to?(:to_reference) - "#{project.to_reference}#{entity.to_reference}" + entity.to_reference(project) else '' end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 4f180456b16..e5b1e6e8bc7 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -82,12 +82,6 @@ module LabelsHelper span.html_safe end - def render_colored_cross_project_label(label, source_project = nil, tooltip: true) - label_suffix = source_project ? source_project.name_with_namespace : label.project.name_with_namespace - label_suffix = " <i>in #{escape_once(label_suffix)}</i>" - render_colored_label(label, label_suffix, tooltip: tooltip) - end - def suggested_colors [ '#0033CC', @@ -166,6 +160,5 @@ module LabelsHelper end # Required for Banzai::Filter::LabelReferenceFilter - module_function :render_colored_label, :render_colored_cross_project_label, - :text_color_for_bg, :escape_once + module_function :render_colored_label, :text_color_for_bg, :escape_once end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index fabbf97d4db..caf6908505e 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -320,6 +320,10 @@ module Ci .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id } end + def detailed_status + Gitlab::Ci::Status::Pipeline::Factory.new(self).fabricate! + end + private def pipeline_data diff --git a/app/models/commit.rb b/app/models/commit.rb index 176c524cf7b..248140f421b 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -92,19 +92,11 @@ class Commit end def to_reference(from_project = nil) - if cross_project_reference?(from_project) - project.to_reference + self.class.reference_prefix + self.id - else - self.id - end + commit_reference(from_project, id) end def reference_link_text(from_project = nil) - if cross_project_reference?(from_project) - project.to_reference + self.class.reference_prefix + self.short_id - else - self.short_id - end + commit_reference(from_project, short_id) end def diff_line_count @@ -329,6 +321,16 @@ class Commit private + def commit_reference(from_project, referable_commit_id) + reference = project.to_reference(from_project) + + if reference.present? + "#{reference}#{self.class.reference_prefix}#{referable_commit_id}" + else + referable_commit_id + end + end + def find_author_by_any_email User.find_by_any_email(author_email.downcase) end diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index ac2477fd973..d9af7f6c139 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -90,21 +90,24 @@ class CommitRange alias_method :id, :to_s def to_reference(from_project = nil) - if cross_project_reference?(from_project) - project.to_reference + self.class.reference_prefix + self.id + project_reference = project.to_reference(from_project) + + if project_reference.present? + project_reference + self.class.reference_prefix + self.id else self.id end end def reference_link_text(from_project = nil) - reference = ref_from + notation + ref_to + project_reference = project.to_reference(from_project) + reference = ref_from + notation + ref_to - if cross_project_reference?(from_project) - reference = project.to_reference + self.class.reference_prefix + reference + if project_reference.present? + project_reference + self.class.reference_prefix + reference + else + reference end - - reference end # Return a Hash of parameters for passing to a URL helper diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb index dee940a3f88..8ba009fe04f 100644 --- a/app/models/concerns/referable.rb +++ b/app/models/concerns/referable.rb @@ -72,17 +72,4 @@ module Referable }x end end - - private - - # Check if a reference is being done cross-project - # - # from_project - Refering Project object - def cross_project_reference?(from_project) - if self.is_a?(Project) - self != from_project - else - from_project && self.project && self.project != from_project - end - end end diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 75a85563235..bbe813db823 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -88,6 +88,10 @@ class Discussion @first_note ||= @notes.first end + def first_note_to_resolve + @first_note_to_resolve ||= notes.detect(&:to_be_resolved?) + end + def last_note @last_note ||= @notes.last end diff --git a/app/models/issue.rb b/app/models/issue.rb index fbf07040301..7fe92051037 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -153,11 +153,7 @@ class Issue < ActiveRecord::Base def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" - if cross_project_reference?(from_project) - reference = project.to_reference + reference - end - - reference + "#{project.to_reference(from_project)}#{reference}" end def referenced_merge_requests(current_user = nil) diff --git a/app/models/label.rb b/app/models/label.rb index d9287f2dc29..d38c37344c9 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -144,9 +144,10 @@ class Label < ActiveRecord::Base # # Examples: # - # Label.first.to_reference # => "~1" - # Label.first.to_reference(format: :name) # => "~\"bug\"" - # Label.first.to_reference(project1, project2) # => "gitlab-org/gitlab-ce~1" + # Label.first.to_reference # => "~1" + # Label.first.to_reference(format: :name) # => "~\"bug\"" + # Label.first.to_reference(project, same_namespace_project) # => "gitlab-ce~1" + # Label.first.to_reference(project, another_namespace_project) # => "gitlab-org/gitlab-ce~1" # # Returns a String # @@ -154,8 +155,8 @@ class Label < ActiveRecord::Base format_reference = label_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" - if cross_project_reference?(source_project, target_project) - source_project.to_reference + reference + if source_project + "#{source_project.to_reference(target_project)}#{reference}" else reference end @@ -169,10 +170,6 @@ class Label < ActiveRecord::Base private - def cross_project_reference?(source_project, target_project) - source_project && target_project && source_project != target_project - end - def issues_count(user, params = {}) params.merge!(subject_foreign_key => subject.id, label_name: title, scope: 'all') IssuesFinder.new(user, params.with_indifferent_access).execute.count diff --git a/app/models/member.rb b/app/models/member.rb index df93aaee847..3b65587c66b 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -63,6 +63,7 @@ class Member < ActiveRecord::Base after_create :send_request, if: :request?, unless: :importing? after_create :create_notification_setting, unless: [:pending?, :importing?] after_create :post_create_hook, unless: [:pending?, :importing?] + after_create :refresh_member_authorized_projects, if: :importing? after_update :post_update_hook, unless: [:pending?, :importing?] after_destroy :post_destroy_hook, unless: :pending? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index bfb016df46d..33b578e12c1 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -176,11 +176,7 @@ class MergeRequest < ActiveRecord::Base def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" - if cross_project_reference?(from_project) - reference = project.to_reference + reference - end - - reference + "#{project.to_reference(from_project)}#{reference}" end def first_commit @@ -480,6 +476,14 @@ class MergeRequest < ActiveRecord::Base @diff_discussions ||= self.notes.diff_notes.discussions end + def resolvable_discussions + @resolvable_discussions ||= diff_discussions.select(&:to_be_resolved?) + end + + def discussions_can_be_resolved_by?(user) + resolvable_discussions.all? { |discussion| discussion.can_resolve?(user) } + end + def find_diff_discussion(discussion_id) notes = self.notes.diff_notes.where(discussion_id: discussion_id).fresh.to_a return if notes.empty? diff --git a/app/models/milestone.rb b/app/models/milestone.rb index c774e69080c..45ca97adad1 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -113,19 +113,16 @@ class Milestone < ActiveRecord::Base # # Examples: # - # Milestone.first.to_reference # => "%1" - # Milestone.first.to_reference(format: :name) # => "%\"goal\"" - # Milestone.first.to_reference(project) # => "gitlab-org/gitlab-ce%1" + # Milestone.first.to_reference # => "%1" + # Milestone.first.to_reference(format: :name) # => "%\"goal\"" + # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1" + # Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1" # def to_reference(from_project = nil, format: :iid) format_reference = milestone_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" - if cross_project_reference?(from_project) - project.to_reference + reference - else - reference - end + "#{project.to_reference(from_project)}#{reference}" end def reference_link_text(from_project = nil) diff --git a/app/models/note.rb b/app/models/note.rb index 5b50ca285c3..08bd08743ef 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -99,7 +99,7 @@ class Note < ActiveRecord::Base end def discussions - Discussion.for_notes(all) + Discussion.for_notes(fresh) end def grouped_diff_discussions diff --git a/app/models/project.rb b/app/models/project.rb index f01cb613b85..9d58aff4033 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -419,7 +419,11 @@ class Project < ActiveRecord::Base def reference_pattern name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR - %r{(?<project>#{name_pattern}/#{name_pattern})} + + %r{ + ((?<namespace>#{name_pattern})\/)? + (?<project>#{name_pattern}) + }x end def trending @@ -650,8 +654,20 @@ class Project < ActiveRecord::Base end end - def to_reference(_from_project = nil) - path_with_namespace + def to_reference(from_project = nil) + if cross_namespace_reference?(from_project) + path_with_namespace + elsif cross_project_reference?(from_project) + path + end + end + + def to_human_reference(from_project = nil) + if cross_namespace_reference?(from_project) + name_with_namespace + elsif cross_project_reference?(from_project) + name + end end def web_url @@ -1327,10 +1343,21 @@ class Project < ActiveRecord::Base private + # Check if a reference is being done cross-project + # + # from_project - Refering Project object + def cross_project_reference?(from_project) + from_project && self != from_project + end + def pushes_since_gc_redis_key "projects/#{id}/pushes_since_gc" end + def cross_namespace_reference?(from_project) + from_project && namespace != from_project.namespace + end + def default_branch_protected? current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE diff --git a/app/models/repository.rb b/app/models/repository.rb index e2e7d08abac..3c4b0212af7 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -85,11 +85,7 @@ class Repository # This method return true if repository contains some content visible in project page. # def has_visible_content? - return @has_visible_content unless @has_visible_content.nil? - - @has_visible_content = cache.fetch(:has_visible_content?) do - branch_count > 0 - end + branch_count > 0 end def commit(ref = 'HEAD') @@ -374,12 +370,6 @@ class Repository return unless empty? expire_method_caches(%i(empty?)) - expire_has_visible_content_cache - end - - def expire_has_visible_content_cache - cache.expire(:has_visible_content?) - @has_visible_content = nil end def lookup_cache @@ -467,7 +457,6 @@ class Repository # Runs code after a new branch has been created. def after_create_branch expire_branches_cache - expire_has_visible_content_cache repository_event(:push_branch) end @@ -481,7 +470,6 @@ class Repository # Runs code after an existing branch has been removed. def after_remove_branch - expire_has_visible_content_cache expire_branches_cache end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 8ff4e7ae718..aa2e3a1ff18 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -67,11 +67,11 @@ class Snippet < ActiveRecord::Base def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{id}" - if cross_project_reference?(from_project) - reference = project.to_reference + reference + if project.present? + "#{project.to_reference(from_project)}#{reference}" + else + reference end - - reference end def self.content_types diff --git a/app/models/user.rb b/app/models/user.rb index b54ce14f0bf..b9bb4a9e3f7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -512,7 +512,7 @@ class User < ActiveRecord::Base end def require_ssh_key? - keys.count == 0 + keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh') end def require_password? diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 8b25332b73c..7b1752df0e1 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -1,6 +1,8 @@ module Ci class BuildPolicy < CommitStatusPolicy def rules + can! :read_build if @subject.project.public_builds? + super # If we can't read build we should also not have that diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 8ac4bd9df6d..d5aadfce76a 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -12,9 +12,6 @@ class ProjectPolicy < BasePolicy guest_access! public_access! - # Allow to read builds for internal projects - can! :read_build if project.public_builds? - if project.request_access_enabled && !(owner || user.admin? || project.team.member?(user) || project_group_member?(user)) can! :request_access @@ -46,6 +43,11 @@ class ProjectPolicy < BasePolicy can! :create_note can! :upload_file can! :read_cycle_analytics + + if project.public_builds? + can! :read_pipeline + can! :read_build + end end def reporter_access! diff --git a/app/services/discussions/base_service.rb b/app/services/discussions/base_service.rb new file mode 100644 index 00000000000..e4dfe6e71bb --- /dev/null +++ b/app/services/discussions/base_service.rb @@ -0,0 +1,4 @@ +module Discussions + class BaseService < ::BaseService + end +end diff --git a/app/services/discussions/resolve_service.rb b/app/services/discussions/resolve_service.rb new file mode 100644 index 00000000000..0437195f588 --- /dev/null +++ b/app/services/discussions/resolve_service.rb @@ -0,0 +1,24 @@ +module Discussions + class ResolveService < Discussions::BaseService + def execute(one_or_more_discussions) + Array(one_or_more_discussions).each { |discussion| resolve_discussion(discussion) } + end + + def resolve_discussion(discussion) + return unless discussion.can_resolve?(current_user) + + discussion.resolve!(current_user) + + MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request) + SystemNoteService.discussion_continued_in_issue(discussion, project, current_user, follow_up_issue) if follow_up_issue + end + + def merge_request + params[:merge_request] + end + + def follow_up_issue + params[:follow_up_issue] + end + end +end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index ce68e433ab8..b5f63cc5a1a 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -120,9 +120,10 @@ class IssuableBaseService < BaseService def merge_slash_commands_into_params!(issuable) description, command_params = SlashCommands::InterpretService.new(project, current_user). - execute(params[:description], issuable) + execute(params[:description], issuable) - params[:description] = description + # Avoid a description already set on an issuable to be overwritten by a nil + params[:description] = description if params.has_key?(:description) params.merge!(command_params) end diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 9ea3ce084ba..742e834df97 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -1,5 +1,13 @@ module Issues class BaseService < ::IssuableBaseService + attr_reader :merge_request_for_resolving_discussions + + def initialize(*args) + super + + @merge_request_for_resolving_discussions ||= params.delete(:merge_request_for_resolving_discussions) + end + def hook_data(issue, action) issue_data = issue.to_hook_data(current_user) issue_url = Gitlab::UrlBuilder.build(issue) diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb new file mode 100644 index 00000000000..a63982f60c8 --- /dev/null +++ b/app/services/issues/build_service.rb @@ -0,0 +1,50 @@ +module Issues + class BuildService < Issues::BaseService + def execute + @issue = project.issues.new(issue_params) + end + + def issue_params_with_info_from_merge_request + return {} unless merge_request_for_resolving_discussions + + { title: title_from_merge_request, description: description_from_merge_request } + end + + def title_from_merge_request + "Follow-up from \"#{merge_request_for_resolving_discussions.title}\"" + end + + def description_from_merge_request + if merge_request_for_resolving_discussions.resolvable_discussions.empty? + return "There are no unresolved discussions. "\ + "Review the conversation in #{merge_request_for_resolving_discussions.to_reference}" + end + + description = "The following discussions from #{merge_request_for_resolving_discussions.to_reference} should be addressed:" + [description, *items_for_discussions].join("\n\n") + end + + def items_for_discussions + merge_request_for_resolving_discussions.resolvable_discussions.map { |discussion| item_for_discussion(discussion) } + end + + def item_for_discussion(discussion) + first_note = discussion.first_note_to_resolve + other_note_count = discussion.notes.size - 1 + creation_time = first_note.created_at.to_s(:medium) + note_url = Gitlab::UrlBuilder.build(first_note) + + discussion_info = "- [ ] #{first_note.author.to_reference} commented in a discussion on [#{creation_time}](#{note_url}): " + discussion_info << " (+#{other_note_count} #{'comment'.pluralize(other_note_count)})" if other_note_count > 0 + + note_without_block_quotes = Banzai::Filter::BlockquoteFenceFilter.new(first_note.note).call + quote = ">>>\n#{note_without_block_quotes}\n>>>" + + [discussion_info, quote].join("\n\n") + end + + def issue_params + @issue_params ||= issue_params_with_info_from_merge_request.merge(params.slice(:title, :description)) + end + end +end diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index ea1690f3e38..d2eb46ac41b 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -4,7 +4,8 @@ module Issues @request = params.delete(:request) @api = params.delete(:api) - @issue = project.issues.new + issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions) + @issue = BuildService.new(project, current_user, issue_attributes).execute create(@issue) end @@ -18,6 +19,17 @@ module Issues notification_service.new_issue(issuable, current_user) todo_service.new_issue(issuable, current_user) user_agent_detail_service.create + + if merge_request_for_resolving_discussions.try(:discussions_can_be_resolved_by?, current_user) + resolve_discussions_in_merge_request(issuable) + end + end + + def resolve_discussions_in_merge_request(issue) + Discussions::ResolveService.new(project, current_user, + merge_request: merge_request_for_resolving_discussions, + follow_up_issue: issue). + execute(merge_request_for_resolving_discussions.resolvable_discussions) end private diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb index dc159de0058..5616edf8b4a 100644 --- a/app/services/merge_requests/merge_when_build_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb @@ -1,5 +1,5 @@ module MergeRequests - class MergeWhenBuildSucceedsService < MergeRequests::BaseService + class MergeWhenPipelineSucceedsService < MergeRequests::BaseService # Marks the passed `merge_request` to be merged when the build succeeds or # updates the params for the automatic merge def execute(merge_request) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index a33845848b4..8b48d90f60b 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -131,14 +131,14 @@ module SystemNoteService create_note(noteable: noteable, project: project, author: author, note: body) end - # Called when 'merge when build succeeds' is executed + # Called when 'merge when pipeline succeeds' is executed def merge_when_build_succeeds(noteable, project, author, last_commit) - body = "enabled an automatic merge when the build for #{last_commit.to_reference(project)} succeeds" + body = "enabled an automatic merge when the pipeline for #{last_commit.to_reference(project)} succeeds" create_note(noteable: noteable, project: project, author: author, note: body) end - # Called when 'merge when build succeeds' is canceled + # Called when 'merge when pipeline succeeds' is canceled def cancel_merge_when_build_succeeds(noteable, project, author) body = 'canceled the automatic merge' @@ -163,6 +163,14 @@ module SystemNoteService create_note(noteable: merge_request, project: project, author: author, note: body) end + def discussion_continued_in_issue(discussion, project, author, issue) + body = "Added #{issue.to_reference} to continue this discussion" + note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body) + note_attributes[:type] = note_attributes.delete(:note_type) + + create_note(note_attributes) + end + # Called when the title of a Noteable is changed # # noteable - Noteable object that responds to `title` diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml index 7bbc75db9ff..c4b748d0ab8 100644 --- a/app/views/admin/abuse_reports/index.html.haml +++ b/app/views/admin/abuse_reports/index.html.haml @@ -4,7 +4,7 @@ .abuse-reports - if @abuse_reports.present? .table-holder - %table.table + %table.table.responsive-table %thead.hidden-sm.hidden-xs %tr %th User @@ -13,8 +13,6 @@ %th Action = render @abuse_reports - else - .no-reports - %span.pull-left - There are no abuse reports! - .pull-left - = emoji_icon 'tada' + .empty-state + .text-center + %h4 There are no abuse reports! #{emoji_icon 'tada'} diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 1db2150f336..e51f4ac1d93 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -113,7 +113,7 @@ %hr .row .col-sm-4 - .light-well + .light-well.well-centered %h4 Projects .data = link_to admin_namespaces_projects_path do @@ -121,7 +121,7 @@ %hr = link_to('New Project', new_project_path, class: "btn btn-new") .col-sm-4 - .light-well + .light-well.well-centered %h4 Users .data = link_to admin_users_path do @@ -129,7 +129,7 @@ %hr = link_to 'New User', new_admin_user_path, class: "btn btn-new" .col-sm-4 - .light-well + .light-well.well-centered %h4 Groups .data = link_to admin_groups_path do @@ -143,7 +143,7 @@ %hr - @projects.each do |project| %p - = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated' + = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60' %span.light.pull-right #{time_ago_with_tooltip(project.created_at)} @@ -152,7 +152,7 @@ %hr - @users.each do |user| %p - = link_to [:admin, user], class: 'str-truncated' do + = link_to [:admin, user], class: 'str-truncated-60' do = user.name %span.light.pull-right #{time_ago_with_tooltip(user.created_at)} @@ -162,7 +162,7 @@ %hr - @groups.each do |group| %p - = link_to [:admin, group], class: 'str-truncated' do + = link_to [:admin, group], class: 'str-truncated-60' do = group.name %span.light.pull-right #{time_ago_with_tooltip(group.created_at)} diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml index 4bf1c9cde3c..2d9588f9d27 100644 --- a/app/views/admin/users/_user.html.haml +++ b/app/views/admin/users/_user.html.haml @@ -1,8 +1,8 @@ -%li.user-row +%li.flex-row .user-avatar = image_tag avatar_icon(user), class: "avatar", alt: '' - .user-details - .user-name + .row-main-content + .user-name.row-title.str-truncated-100 = link_to user.name, [:admin, user] - if user.blocked? %span.label.label-danger blocked @@ -12,7 +12,7 @@ %span.label.label-default External - if user == current_user %span It's you! - .user-email + .row-second-line.str-truncated-100 = mail_to user.email, user.email .controls = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn' diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index d3038ae644f..4dc44225d49 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -68,7 +68,7 @@ %small.badge= number_with_delimiter(User.without_projects.count) .fade-right - %ul.users-list.content-list + %ul.flex-list.content-list - if @users.empty? %li .nothing-here-block No users found. diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml index 84b9ceb23b3..dd6b7303493 100644 --- a/app/views/admin/users/projects.html.haml +++ b/app/views/admin/users/projects.html.haml @@ -7,7 +7,7 @@ %ul.well-list - @user.groups.each do |group| %li - %strong= group.name + %strong= link_to group.name, admin_group_path(group) – access to #{pluralize(group.projects.count, 'project')} diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml index 20cd7b0179d..fb70d158096 100644 --- a/app/views/devise/confirmations/almost_there.haml +++ b/app/views/devise/confirmations/almost_there.haml @@ -1,12 +1,13 @@ -.well-confirmation.text-center +.well-confirmation.text-center.append-bottom-20 %h1.prepend-top-0 Almost there... - %p.lead + %p.lead.append-bottom-20 Please check your email to confirm your account + %hr - if current_application_settings.after_sign_up_text.present? .well-confirmation.text-center = markdown_field(current_application_settings, :after_sign_up_text) -%p.confirmation-content.text-center +%p.text-center No confirmation email received? Please check your spam folder or .append-bottom-20.prepend-top-20.text-center %a.btn.btn-lg.btn-success{ href: new_user_confirmation_path } diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index f7edb47b666..f3539fd372d 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -31,7 +31,7 @@ = link_to merge_requests_group_path(@group), title: 'Merge Requests' do %span Merge Requests - - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute + - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute %span.badge.count= number_with_delimiter(merge_requests.count) = nav_link(controller: [:group_members]) do = link_to group_group_members_path(@group), title: 'Members' do diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index cadfe5a3e30..f63802ac88b 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -31,7 +31,7 @@ .light = commit_author_link(commit, avatar: false) - authored + committed #{time_ago_with_tooltip(commit.committed_date)} %td.line-numbers - line_count = blame_group[:lines].count diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 7b995bd8735..40bfa01a45a 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,42 +1,41 @@ - if !project.empty_repo? && can?(current_user, :download_code, project) - %span{class: 'download-button'} - .dropdown.inline - %button.btn{ 'data-toggle' => 'dropdown' } - = icon('download') - = icon("caret-down") - %span.sr-only - Select Archive Format - %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } - %li.dropdown-header Source code - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do - %i.fa.fa-download - %span Download zip - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.gz - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar.bz2 - %li - = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do - %i.fa.fa-download - %span Download tar + .dropdown.inline.download-button + %button.btn{ 'data-toggle' => 'dropdown' } + = icon('download') + = icon("caret-down") + %span.sr-only + Select Archive Format + %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } + %li.dropdown-header Source code + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do + %i.fa.fa-download + %span Download zip + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.gz + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.bz2 + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar - - pipeline = project.pipelines.latest_successful_for(ref) - - if pipeline - - artifacts = pipeline.builds.latest.with_artifacts - - if artifacts.any? - %li.dropdown-header Artifacts - - unless pipeline.latest? - - latest_pipeline = project.pipeline_for(ref) - %li - .unclickable= ci_status_for_statuseable(latest_pipeline) - %li.dropdown-header Previous Artifacts - - artifacts.each do |job| - %li - = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do - %i.fa.fa-download - %span Download '#{job.name}' + - pipeline = project.pipelines.latest_successful_for(ref) + - if pipeline + - artifacts = pipeline.builds.latest.with_artifacts + - if artifacts.any? + %li.dropdown-header Artifacts + - unless pipeline.latest? + - latest_pipeline = project.pipeline_for(ref) + %li + .unclickable= ci_status_for_statuseable(latest_pipeline) + %li.dropdown-header Previous Artifacts + - artifacts.each do |job| + %li + = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do + %i.fa.fa-download + %span Download '#{job.name}' diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 4c7b14a04db..0f08f4e8592 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -1,12 +1,13 @@ - status = pipeline.status +- detailed_status = pipeline.detailed_status - show_commit = local_assigns.fetch(:show_commit, true) - show_branch = local_assigns.fetch(:show_branch, true) %tr.commit %td.commit-link - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "ci-status ci-#{status}" do - = ci_icon_for_status(status) - = ci_label_for_status(status) + = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "ci-status ci-#{detailed_status}" do + = ci_icon_for_status(detailed_status) + = ci_text_for_status(detailed_status) %td = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 12096941209..a940515fadf 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -37,5 +37,5 @@ = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author)) = commit_author_link(commit, avatar: false, size: 24) - authored + committed #{time_ago_with_tooltip(commit.committed_date)} diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 6d9b91ad0e7..9ab7971b56c 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -8,14 +8,13 @@ = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline' = ci_label_for_status(status) for - - commit = @merge_request.diff_head_commit = succeed "." do = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" %span.ci-coverage - elsif @merge_request.has_ci? - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX - - # Remove in later versions when services like Jenkins will set CI status via Commit status API + - # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API .mr-widget-heading - %w[success skipped canceled failed running pending].each do |status| .ci_widget{class: "ci-#{status}", style: "display:none"} diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index ce43ca3a286..435fe835fae 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -9,7 +9,7 @@ - if @pipeline && @pipeline.active? %span.btn-group = button_tag class: "btn btn-create js-merge-button merge_when_build_succeeds" do - Merge When Build Succeeds + Merge When Pipeline Succeeds - unless @project.only_allow_merge_if_build_succeeds? = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do = icon('caret-down') @@ -19,7 +19,7 @@ %li = link_to "#", class: "merge_when_build_succeeds" do = icon('check fw') - Merge When Build Succeeds + Merge When Pipeline Succeeds %li = link_to "#", class: "accept_merge_request" do = icon('warning fw') diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index 1aeb12e4661..072d01d144e 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -1,6 +1,6 @@ %h4 Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} - to be merged automatically when the build succeeds. + to be merged automatically when the pipeline succeeds. %div %p = succeed '.' do diff --git a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml index 35d5677ee37..e094f97f3b6 100644 --- a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml +++ b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml @@ -3,4 +3,8 @@ This merge request has unresolved discussions %p - Please resolve these discussions to allow this merge request to be merged.
\ No newline at end of file + Please resolve these discussions + - if @project.issues_enabled? && can?(current_user, :create_issue, @project) + or + = link_to "open an issue to resolve them later", new_namespace_project_issue_path(@project.namespace, @project, merge_request_for_resolving_discussions: @merge_request.iid) + to allow this merge request to be merged. diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 095bd254d6b..229bdfb0e8d 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,6 +1,6 @@ .page-content-header .header-main-content - = ci_status_with_icon(@pipeline.status) + = ci_status_with_icon(@pipeline.detailed_status) %strong Pipeline ##{@commit.pipelines.last.id} triggered #{time_ago_with_tooltip(@commit.authored_date)} by = author_avatar(@commit, size: 24) diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 155af755759..12facb6eb73 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -3,8 +3,16 @@ = render "projects/commits/head" %div{ class: container_class } - .sub-header-block - .pull-right.tag-buttons + .top-area.multi-line + .nav-text + .title + %span.item-title= @tag.name + - if @commit + = render 'projects/branches/commit', commit: @commit, project: @project + - else + Cant find HEAD commit for this tag + + .nav-controls - if can?(current_user, :push_code, @project) = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Edit release notes' do = icon("pencil") @@ -15,15 +23,8 @@ = render 'projects/buttons/download', project: @project, ref: @tag.name - if can?(current_user, :admin_project, @project) .pull-right - = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do + = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do %i.fa.fa-trash-o - .tag-info.append-bottom-10 - .title - %span.item-title= @tag.name - - if @commit - = render 'projects/branches/commit', commit: @commit, project: @project - - else - Cant find HEAD commit for this tag - if @tag.message.present? %pre.body = strip_gpg_signature(@tag.message) diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 2f05093f435..bdb00bfa33c 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -42,6 +42,21 @@ = render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form +- if @merge_request_for_resolving_discussions + .form-group + .col-sm-10.col-sm-offset-2 + - if @merge_request_for_resolving_discussions.discussions_can_be_resolved_by?(current_user) + = icon('exclamation-triangle') + Creating this issue will mark all discussions in + = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions) + as resolved. + = hidden_field_tag 'merge_request_for_resolving_discussions', @merge_request_for_resolving_discussions.iid + - else + = icon('exclamation-triangle') + You can't automatically mark all discussions in + = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions) + as resolved. Ask someone with sufficient rights to resolve the them. + - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) .row-content-block{class: (is_footer ? "footer-block" : "middle-block")} - if issuable.new_record? diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index 5527a2f889a..0af92b59584 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -4,22 +4,22 @@ %ul.nav-links.issues-state-filters %li{class: ("active" if params[:state] == 'opened')} - = link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do + = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened." do #{issuables_state_counter_text(type, :opened)} - if type == :merge_requests %li{class: ("active" if params[:state] == 'merged')} - = link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do + = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.' do #{issuables_state_counter_text(type, :merged)} %li{class: ("active" if params[:state] == 'closed')} - = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do + = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.' do #{issuables_state_counter_text(type, :closed)} - else %li{class: ("active" if params[:state] == 'closed')} - = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do + = link_to page_filter_path(state: 'closed', label: true), id: 'state-all', title: 'Filter by issues that are currently closed.' do #{issuables_state_counter_text(type, :closed)} %li{class: ("active" if params[:state] == 'all')} - = link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do + = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}." do #{issuables_state_counter_text(type, :all)} diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb index 2aa6fff24da..cc0eb708cf9 100644 --- a/app/workers/pipeline_success_worker.rb +++ b/app/workers/pipeline_success_worker.rb @@ -4,7 +4,7 @@ class PipelineSuccessWorker def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| - MergeRequests::MergeWhenBuildSucceedsService + MergeRequests::MergeWhenPipelineSucceedsService .new(pipeline.project, nil) .trigger(pipeline) end |