diff options
141 files changed, 1810 insertions, 797 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 12a3e63ed2e..86b30d2832d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.14.2 (2016-12-01) + +- Remove caching of events data. !6578 +- Rephrase some system notes to be compatible with new system note style. !7692 +- Pass tag SHA to post-receive hook when tag is created via UI. !7700 +- Prevent error when submitting a merge request and pipeline is not defined. !7707 +- Fixes system note style in commit discussion. !7721 +- Use a Redis lease for updating authorized projects. !7733 +- Refactor JiraService by moving code out of JiraService#execute method. !7756 +- Update GitLab Workhorse to v1.0.1. !7759 +- Fix pipelines info being hidden in merge request widget. !7808 +- Fixed commit timeago not rendering after initial page. +- Fix for error thrown in cycle analytics events if build has not started. +- Fixed issue boards issue sorting when dragging issue into list. +- Allow access to the wiki with git when repository feature disabled. +- Fixed timeago not rendering when resolving a discussion. +- Update Sidekiq-cron to fix compatibility issues with Sidekiq 4.2.1. +- Timeout creating and viewing merge request for binary file. +- Gracefully recover from Redis connection failures in Sidekiq initializer. + ## 8.14.1 (2016-11-28) - Fix deselecting calendar days on contribution graph. !6453 (ClemMakesApps) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 76f3c6506ed..cfab4721f4b 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -57,32 +57,11 @@ (function () { document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch); - window.addEventListener('hashchange', gl.utils.shiftWindow); - - // automatically adjust scroll position for hash urls taking the height of the navbar into account - // https://github.com/twitter/bootstrap/issues/1768 - window.adjustScroll = function() { - var navbar = document.querySelector('.navbar-gitlab'); - var subnav = document.querySelector('.layout-nav'); - var fixedTabs = document.querySelector('.js-tabs-affix'); - - adjustment = 0; - if (navbar) adjustment -= navbar.offsetHeight; - if (subnav) adjustment -= subnav.offsetHeight; - if (fixedTabs) adjustment -= fixedTabs.offsetHeight; - - return scrollBy(0, adjustment); - }; - - window.addEventListener("hashchange", adjustScroll); - - window.onload = function () { - // Scroll the window to avoid the topnav bar - // https://github.com/twitter/bootstrap/issues/1768 - if (location.hash) { - return setTimeout(adjustScroll, 100); - } - }; + window.addEventListener('hashchange', gl.utils.handleLocationHash); + window.addEventListener('load', function onLoad() { + window.removeEventListener('load', onLoad, false); + gl.utils.handleLocationHash(); + }, false); $(function () { var $body = $('body'); diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6 index d5cb6164e0b..1644a772737 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js.es6 +++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6 @@ -47,7 +47,7 @@ new gl.DueDateSelectors(); new LabelsSelect(); new Sidebar(); - new Subscription('.subscription'); + gl.Subscription.bindAll('.subscription'); } }); })(); diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 16df4b0b005..ab521c6c1fc 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -135,8 +135,18 @@ new TreeView(); } break; + case 'projects:pipelines:builds': case 'projects:pipelines:show': - new gl.Pipelines(); + const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; + + new gl.Pipelines({ + initTabs: true, + tabsOptions: { + action: controllerAction, + defaultAction: 'pipelines', + parentEl: '.pipelines-tabs', + }, + }); break; case 'groups:activity': new gl.Activities(); @@ -262,7 +272,7 @@ new NotificationsDropdown(); break; case 'wikis': - new Wikis(); + new gl.Wikis(); shortcut_handler = new ShortcutsNavigation(); new ZenMode(); new GLForm($('.wiki-form')); diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6 index 6d9b0c4bc3e..3f12ad9ff9f 100644 --- a/app/assets/javascripts/extensions/element.js.es6 +++ b/app/assets/javascripts/extensions/element.js.es6 @@ -1,9 +1,20 @@ /* global Element */ -/* eslint-disable consistent-return, max-len */ - -Element.prototype.matches = Element.prototype.matches || Element.prototype.msMatchesSelector; +/* eslint-disable consistent-return, max-len, no-empty, no-plusplus, func-names */ Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) { if (!selectedElement) return; return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement); }; + +Element.prototype.matches = Element.prototype.matches || + Element.prototype.matchesSelector || + Element.prototype.mozMatchesSelector || + Element.prototype.msMatchesSelector || + Element.prototype.oMatchesSelector || + Element.prototype.webkitMatchesSelector || + function (s) { + const matches = (this.document || this.ownerDocument).querySelectorAll(s); + let i = matches.length; + while (--i >= 0 && matches.item(i) !== this) {} + return i > -1; + }; diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 new file mode 100644 index 00000000000..e810ee85bd3 --- /dev/null +++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 @@ -0,0 +1,113 @@ +/** + * Linked Tabs + * + * Handles persisting and restores the current tab selection and content. + * Reusable component for static content. + * + * ### Example Markup + * + * <ul class="nav-links tab-links"> + * <li class="active"> + * <a data-action="tab1" data-target="#tab1" data-toggle="tab" href="/path/tab1"> + * Tab 1 + * </a> + * </li> + * <li class="groups-tab"> + * <a data-action="tab2" data-target="#tab2" data-toggle="tab" href="/path/tab2"> + * Tab 2 + * </a> + * </li> + * + * + * <div class="tab-content"> + * <div class="tab-pane" id="tab1"> + * Tab 1 Content + * </div> + * <div class="tab-pane" id="tab2"> + * Tab 2 Content + * </div> + * </div> + * + * + * ### How to use + * + * new window.gl.LinkedTabs({ + * action: "#{controller.action_name}", + * defaultAction: 'tab1', + * parentEl: '.tab-links' + * }); + */ + +(() => { + window.gl = window.gl || {}; + + window.gl.LinkedTabs = class LinkedTabs { + /** + * Binds the events and activates de default tab. + * + * @param {Object} options + */ + constructor(options) { + this.options = options || {}; + + this.defaultAction = this.options.defaultAction; + this.action = this.options.action || this.defaultAction; + + if (this.action === 'show') { + this.action = this.defaultAction; + } + + this.currentLocation = window.location; + + const tabSelector = `${this.options.parentEl} a[data-toggle="tab"]`; + + // since this is a custom event we need jQuery :( + $(document) + .off('shown.bs.tab', tabSelector) + .on('shown.bs.tab', tabSelector, e => this.tabShown(e)); + + this.activateTab(this.action); + } + + /** + * Handles the `shown.bs.tab` event to set the currect url action. + * + * @param {type} evt + * @return {Function} + */ + tabShown(evt) { + const source = evt.target.getAttribute('href'); + + return this.setCurrentAction(source); + } + + /** + * Updates the URL with the path that matched the given action. + * + * @param {String} source + * @return {String} + */ + setCurrentAction(source) { + const copySource = source; + + copySource.replace(/\/+$/, ''); + + const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`; + + history.replaceState({ + turbolinks: true, + url: newState, + }, document.title, newState); + return newState; + } + + /** + * Given the current action activates the correct tab. + * http://getbootstrap.com/javascript/#tab-show + * Note: Will trigger `shown.bs.tab` + */ + activateTab() { + return $(`${this.options.parentEl} a[data-action='${this.action}']`).tab('show'); + } + }; +})(); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index d83c41fae9d..c5846068b07 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, padded-blocks, max-len, prefer-template */ (function() { (function(w) { var base; @@ -94,10 +94,35 @@ return $(document).off('scroll'); }; - w.gl.utils.shiftWindow = function() { - return w.scrollBy(0, -100); - }; + // automatically adjust scroll position for hash urls taking the height of the navbar into account + // https://github.com/twitter/bootstrap/issues/1768 + w.gl.utils.handleLocationHash = function() { + var hash = w.gl.utils.getLocationHash(); + if (!hash) return; + + var navbar = document.querySelector('.navbar-gitlab'); + var subnav = document.querySelector('.layout-nav'); + var fixedTabs = document.querySelector('.js-tabs-affix'); + var adjustment = 0; + if (navbar) adjustment -= navbar.offsetHeight; + if (subnav) adjustment -= subnav.offsetHeight; + + // scroll to user-generated markdown anchor if we cannot find a match + if (document.getElementById(hash) === null) { + var target = document.getElementById('user-content-' + hash); + if (target && target.scrollIntoView) { + target.scrollIntoView(true); + window.scrollBy(0, adjustment); + } + } else { + // only adjust for fixedTabs when not targeting user-generated content + if (fixedTabs) { + adjustment -= fixedTabs.offsetHeight; + } + window.scrollBy(0, adjustment); + } + }; gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) { return $tooltipEl.tooltip('destroy').attr('title', newTitle).tooltip('fixTitle'); diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index a84db9c0233..72c6c4a1fcd 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -1,8 +1,15 @@ +//= require lib/utils/bootstrap_linked_tabs + /* eslint-disable */ ((global) => { class Pipelines { - constructor() { + constructor(options) { + + if (options.initTabs && options.tabsOptions) { + new global.LinkedTabs(options.tabsOptions); + } + this.addMarginToBuildColumns(); } diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js deleted file mode 100644 index 6d75688deeb..00000000000 --- a/app/assets/javascripts/subscription.js +++ /dev/null @@ -1,52 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, vars-on-top, no-unused-vars, one-var, one-var-declaration-per-line, camelcase, consistent-return, no-undef, padded-blocks, max-len */ -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.Subscription = (function() { - function Subscription(container) { - this.toggleSubscription = bind(this.toggleSubscription, this); - var $container; - this.$container = $(container); - this.url = this.$container.attr('data-url'); - this.subscribe_button = this.$container.find('.js-subscribe-button'); - this.subscription_status = this.$container.find('.subscription-status'); - this.subscribe_button.unbind('click').click(this.toggleSubscription); - } - - Subscription.prototype.toggleSubscription = function(event) { - var action, btn, current_status; - btn = $(event.currentTarget); - action = btn.find('span').text(); - current_status = this.subscription_status.attr('data-status'); - btn.addClass('disabled'); - - if ($('html').hasClass('issue-boards-page')) { - this.url = this.$container.attr('data-url'); - } - - return $.post(this.url, (function(_this) { - return function() { - var status; - btn.removeClass('disabled'); - - if ($('html').hasClass('issue-boards-page')) { - Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'subscribed', !gl.issueBoards.BoardsStore.detail.issue.subscribed); - } else { - status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed'; - _this.subscription_status.attr('data-status', status); - action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe'; - btn.find('span').text(action); - _this.subscription_status.find('>div').toggleClass('hidden'); - if (btn.attr('data-original-title')) { - return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle'); - } - } - }; - })(this)); - }; - - return Subscription; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/subscription.js.es6 b/app/assets/javascripts/subscription.js.es6 new file mode 100644 index 00000000000..62d1604fe9e --- /dev/null +++ b/app/assets/javascripts/subscription.js.es6 @@ -0,0 +1,50 @@ +/* global Vue */ + +(() => { + class Subscription { + constructor(containerElm) { + this.containerElm = containerElm; + + const subscribeButton = containerElm.querySelector('.js-subscribe-button'); + if (subscribeButton) { + // remove class so we don't bind twice + subscribeButton.classList.remove('js-subscribe-button'); + subscribeButton.addEventListener('click', this.toggleSubscription.bind(this)); + } + } + + toggleSubscription(event) { + const button = event.currentTarget; + const buttonSpan = button.querySelector('span'); + if (!buttonSpan || button.classList.contains('disabled')) { + return; + } + button.classList.add('disabled'); + + const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe'; + const toggleActionUrl = this.containerElm.dataset.url; + + $.post(toggleActionUrl, () => { + button.classList.remove('disabled'); + + // hack to allow this to work with the issue boards Vue object + if (document.querySelector('html').classList.contains('issue-boards-page')) { + Vue.set( + gl.issueBoards.BoardsStore.detail.issue, + 'subscribed', + !gl.issueBoards.BoardsStore.detail.issue.subscribed, + ); + } else { + buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe'; + } + }); + } + + static bindAll(selector) { + [].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm)); + } + } + + window.gl = window.gl || {}; + window.gl.Subscription = Subscription; +})(); diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js deleted file mode 100644 index 5dd853389c2..00000000000 --- a/app/assets/javascripts/wikis.js +++ /dev/null @@ -1,38 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, consistent-return, one-var, one-var-declaration-per-line, no-undef, prefer-template, padded-blocks, max-len */ - -/*= require latinise */ - -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.Wikis = (function() { - function Wikis() { - this.slugify = bind(this.slugify, this); - $('.new-wiki-page').on('submit', (function(_this) { - return function(e) { - var field, path, slug; - $('[data-error~=slug]').addClass('hidden'); - field = $('#new_wiki_path'); - slug = _this.slugify(field.val()); - if (slug.length > 0) { - path = field.attr('data-wikis-path'); - location.href = path + '/' + slug; - return e.preventDefault(); - } - }; - })(this)); - } - - Wikis.prototype.dasherize = function(value) { - return value.replace(/[_\s]+/g, '-'); - }; - - Wikis.prototype.slugify = function(value) { - return this.dasherize(value.trim().toLowerCase().latinise()); - }; - - return Wikis; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6 new file mode 100644 index 00000000000..ecff5fd5bf4 --- /dev/null +++ b/app/assets/javascripts/wikis.js.es6 @@ -0,0 +1,73 @@ +/* eslint-disable no-param-reassign */ +/* global Breakpoints */ + +/*= require latinise */ +/*= require breakpoints */ +/*= require jquery.nicescroll */ + +((global) => { + const dasherize = str => str.replace(/[_\s]+/g, '-'); + const slugify = str => dasherize(str.trim().toLowerCase().latinise()); + + class Wikis { + constructor() { + this.bp = Breakpoints.get(); + this.sidebarEl = document.querySelector('.js-wiki-sidebar'); + this.sidebarExpanded = false; + $(this.sidebarEl).niceScroll(); + + const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle'); + for (let i = 0; i < sidebarToggles.length; i += 1) { + sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e)); + } + + this.newWikiForm = document.querySelector('form.new-wiki-page'); + if (this.newWikiForm) { + this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e)); + } + + window.addEventListener('resize', () => this.renderSidebar()); + this.renderSidebar(); + } + + handleNewWikiSubmit(e) { + if (!this.newWikiForm) return; + + const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); + const slug = slugify(slugInput.value); + + if (slug.length > 0) { + const wikisPath = slugInput.getAttribute('data-wikis-path'); + window.location.href = `${wikisPath}/${slug}`; + e.preventDefault(); + } + } + + handleToggleSidebar(e) { + e.preventDefault(); + this.sidebarExpanded = !this.sidebarExpanded; + this.renderSidebar(); + } + + sidebarCanCollapse() { + const bootstrapBreakpoint = this.bp.getBreakpointSize(); + return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; + } + + renderSidebar() { + if (!this.sidebarEl) return; + const { classList } = this.sidebarEl; + if (this.sidebarExpanded || !this.sidebarCanCollapse()) { + if (!classList.contains('right-sidebar-expanded')) { + classList.remove('right-sidebar-collapsed'); + classList.add('right-sidebar-expanded'); + } + } else if (classList.contains('right-sidebar-expanded')) { + classList.add('right-sidebar-collapsed'); + classList.remove('right-sidebar-expanded'); + } + } + } + + global.Wikis = Wikis; +})(window.gl || (window.gl = {})); diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index 8642b7530e2..81852158b94 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -2,7 +2,7 @@ padding-left: 0; padding-right: 0; - @media (min-width: $screen-sm-min) and (max-width: $screen-lg-min) { + @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { overflow-x: scroll; } } diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index a9006de6d3e..eadb9409fee 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -38,7 +38,7 @@ } } -@media (max-width: $screen-md-min) { +@media (max-width: $screen-sm-max) { ul.notes { .flash-container.timeline-content { margin-left: 0; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index e83a1f7ad68..a01899ccbd2 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -98,7 +98,7 @@ label { } } - @media(max-width: $screen-sm-min) { + @media(max-width: $screen-xs-max) { padding: 0 $gl-padding; .control-label, diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index 91ab1503439..c5e5dad574d 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -21,7 +21,6 @@ background: $color-darker; } - .sidebar-header, .sidebar-action-buttons { color: $color-light; background-color: lighten($color-darker, 5%); diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 16ecf466931..f9bcbbf2ca5 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -228,7 +228,7 @@ header { } .page-sidebar-pinned.right-sidebar-expanded { - @media (max-width: $screen-lg-min) { + @media (max-width: $screen-md-max) { .header-content .title { width: 300px; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 1839ffa0976..98f72e58c23 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -123,7 +123,7 @@ line-height: 28px; /* Small devices (phones, tablets, 768px and lower) */ - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { width: 100%; } } diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss index cb2c351c368..b37c1d0d670 100644 --- a/app/assets/stylesheets/framework/pagination.scss +++ b/app/assets/stylesheets/framework/pagination.scss @@ -43,7 +43,7 @@ /** * Small screen pagination */ -@media (max-width: $screen-xs) { +@media (max-width: $screen-xs-min) { .gl-pagination { .pagination li a { padding: 6px 10px; @@ -62,7 +62,7 @@ /** * Medium screen pagination */ -@media (min-width: $screen-xs) and (max-width: $screen-md-max) { +@media (min-width: $screen-xs-min) and (max-width: $screen-md-max) { .gl-pagination { .page { display: none; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 44c445c0543..4269d365578 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -59,11 +59,6 @@ padding: 0 !important; } - .sidebar-header { - padding: 11px 22px 12px; - font-size: 20px; - } - li { &.separate-item { padding-top: 10px; @@ -220,7 +215,7 @@ header.header-sidebar-pinned { padding-right: 0; @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - &:not(.build-sidebar) { + &:not(.build-sidebar):not(.wiki-sidebar) { padding-right: $sidebar_collapsed_width; } } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 070e42d63d2..aa604b1cd19 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -182,6 +182,7 @@ left: -16px; position: absolute; text-decoration: none; + outline: none; &::after { content: image-url('icon_anchor.svg'); diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 4327f8bf640..82f36f24867 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -325,7 +325,6 @@ } .issuable-header-text { - width: 100%; padding-right: 35px; > strong { diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 498a8f68e49..0b4930ec98f 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -53,7 +53,7 @@ border-bottom: none; position: relative; - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { padding: 6px 0 24px; } } @@ -61,7 +61,7 @@ .column { text-align: center; - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { padding: 15px 0; } @@ -78,7 +78,7 @@ } &:last-child { - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { text-align: center; } } @@ -156,7 +156,7 @@ } .inner-content { - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { padding: 0 28px; text-align: center; } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 4b382e8adaf..de3d2ba549f 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -8,7 +8,7 @@ font-size: 34px; } -@media (max-width: $screen-sm-min) { +@media (max-width: $screen-xs-max) { .environments-container { width: 100%; overflow: auto; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 57d028cec8c..a9af7af59e2 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -49,14 +49,14 @@ padding: 50px 100px; overflow: hidden; - @media (max-width: $screen-md-min) { + @media (max-width: $screen-sm-max) { padding: 50px 0; } svg { float: right; - @media (max-width: $screen-md-min) { + @media (max-width: $screen-sm-max) { float: none; display: block; width: 250px; @@ -71,7 +71,7 @@ width: 460px; margin-top: 120px; - @media (max-width: $screen-md-min) { + @media (max-width: $screen-sm-max) { float: none; margin-top: 60px; width: auto; diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 8843d1463db..dfc6079bd15 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -123,7 +123,7 @@ padding: 20px 0; } -@media (max-width: $screen-sm-min) { +@media (max-width: $screen-xs-max) { .milestone-actions { @include clearfix(); padding-top: $gl-vert-padding; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 16ddef481bd..65775c45e5b 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -111,7 +111,7 @@ text-align: center; font-size: 13px; - @media (max-width: $screen-md-min) { + @media (max-width: $screen-sm-max) { // On smaller devices the warning becomes the fourth item in the list, // rather than centering, and grows to span the full width of the // comment area. diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 15ec8be831e..56a798a7b6d 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -253,7 +253,7 @@ ul.notes { } .page-sidebar-pinned.right-sidebar-expanded { - @media (max-width: $screen-lg-min) { + @media (max-width: $screen-md-max) { .note-header { .note-headline-light { display: block; diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 6fab97a71aa..f8677f93fe0 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -180,7 +180,7 @@ .modal-dialog { width: 380px; - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { width: auto; } @@ -261,4 +261,4 @@ table.u2f-registrations { td:not(:last-child) { border-right: solid 1px transparent; } -}
\ No newline at end of file +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 0562ee7b178..1cf7587dbb4 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -189,7 +189,7 @@ } .download-button { - @media (max-width: $screen-lg-min) { + @media (max-width: $screen-md-max) { margin-left: 0; } } diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss index 69288b31cc4..779db77da60 100644 --- a/app/assets/stylesheets/pages/stat_graph.scss +++ b/app/assets/stylesheets/pages/stat_graph.scss @@ -37,7 +37,7 @@ @include make-md-column(6); margin-top: 10px; - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-xs-max) { width: 100%; } } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 2b836fa1f4a..20ad63be045 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -31,7 +31,7 @@ .last-commit { @include str-truncated(506px); - @media (min-width: $screen-sm-max) and (max-width: $screen-md-max) { + @media (min-width: $screen-md-min) and (max-width: $screen-md-max) { @include str-truncated(450px); } diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index dfaeba41cf6..b9f81533150 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -4,3 +4,128 @@ margin-right: auto; padding-right: 7px; } + +.wiki-page-header { + @extend .top-area; + position: relative; + + .wiki-page-title { + margin: 0; + font-size: 22px; + } + + .wiki-last-edit-by { + color: $gl-gray-light; + + strong { + color: $gl-text-color; + } + } + + .light { + font-weight: normal; + color: $gl-gray-light; + } + + .git-access-header { + padding: 16px 40px 11px 0; + line-height: 28px; + font-size: 18px; + } + + .git-clone-holder { + width: 100%; + padding-bottom: 40px; + } + + button.sidebar-toggle { + position: absolute; + right: 0; + top: 11px; + display: block; + } + + @media (min-width: $screen-sm-min) { + &.has-sidebar-toggle { + padding-right: 40px; + } + + .git-clone-holder { + width: 480px; + } + + .nav-controls { + width: auto; + min-width: 50%; + white-space: nowrap; + } + } + + @media (min-width: $screen-md-min) { + &.has-sidebar-toggle { + padding-right: 0; + } + + button.sidebar-toggle { + display: none; + } + } +} + +.wiki-git-access { + margin: $gl-padding 0; + + h3 { + font-size: 22px; + font-weight: normal; + margin-top: 1.4em; + } +} + +.right-sidebar.wiki-sidebar { + padding: $gl-padding 0; + + &.right-sidebar-collapsed { + display: none; + } + + .blocks-container { + padding: 0 $gl-padding; + } + + .block { + width: 100%; + } + + a { + color: $layout-link-gray; + + &:hover, + &.active { + color: $black; + } + } + + .active > a { + color: $black; + } + + ul.wiki-pages, + ul.wiki-pages li { + list-style: none; + padding: 0; + margin: 0; + } + + ul.wiki-pages li { + margin: 5px 0 10px; + } + + .wiki-sidebar-header { + padding: 0 $gl-padding $gl-padding; + + .gutter-toggle { + margin-top: 0; + } + } +} diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index a10cdcce72b..37feff79999 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -7,8 +7,10 @@ class HelpController < ApplicationController @help_index = File.read(Rails.root.join('doc', 'README.md')) # Prefix Markdown links with `help/` unless they are external links - # See http://rubular.com/r/MioSrVLK3S - @help_index.gsub!(%r{(\]\()(?!.+://)([^\)\(]+\))}, '\1/help/\2') + # See http://rubular.com/r/X3baHTbPO2 + @help_index.gsub!(%r{(?<delim>\]\()(?!.+://)(?!/)(?<link>[^\)\(]+\))}) do + "#{$~[:delim]}#{Gitlab.config.gitlab.relative_url_root}/help/#{$~[:link]}" + end end def show diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index f47df8b623b..d2cef52842c 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -492,7 +492,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def validates_merge_request # Show git not found page # if there is no saved commits between source & target branch - if @merge_request.commits.blank? + if @merge_request.has_no_commits? # and if target branch doesn't exist return invalid_mr unless @merge_request.target_branch_exists? end @@ -500,7 +500,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_show_vars @noteable = @merge_request - @commits_count = @merge_request.commits.count + @commits_count = @merge_request.commits_count if @merge_request.locked_long_ago? @merge_request.unlock_mr diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 533af80aee0..85188cfdd4c 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -1,6 +1,6 @@ class Projects::PipelinesController < Projects::ApplicationController before_action :pipeline, except: [:index, :new, :create] - before_action :commit, only: [:show] + before_action :commit, only: [:show, :builds] before_action :authorize_read_pipeline! before_action :authorize_create_pipeline!, only: [:new, :create] before_action :authorize_update_pipeline!, only: [:retry, :cancel] @@ -32,6 +32,14 @@ class Projects::PipelinesController < Projects::ApplicationController def show end + def builds + respond_to do |format| + format.html do + render 'show' + end + end + end + def retry pipeline.retry_failed(current_user) diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 177ccf5eec9..c3353446fd1 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -115,6 +115,8 @@ class Projects::WikisController < Projects::ApplicationController # Call #wiki to make sure the Wiki Repo is initialized @project_wiki.wiki + + @sidebar_wiki_pages = @project_wiki.pages.first(15) rescue ProjectWiki::CouldNotCreateWikiError flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." redirect_to project_path(@project) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 9a74e36870b..001c83ccb4b 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -49,6 +49,32 @@ class IssuableFinder execute.find_by(*params) end + # We often get counts for each state by running a query per state, and + # counting those results. This is typically slower than running one query + # (even if that query is slower than any of the individual state queries) and + # grouping and counting within that query. + # + def count_by_state + count_params = params.merge(state: nil, sort: nil) + labels_count = label_names.any? ? label_names.count : 1 + finder = self.class.new(current_user, count_params) + counts = Hash.new(0) + + # Searching by label includes a GROUP BY in the query, but ours will be last + # because it is added last. Searching by multiple labels also includes a row + # per issuable, so we have to count those in Ruby - which is bad, but still + # better than performing multiple queries. + # + finder.execute.reorder(nil).group(:state).count.each do |key, value| + counts[Array(key).last.to_sym] += value / labels_count + end + + counts[:all] = counts.values.sum + counts[:opened] += counts[:reopened] + + counts + end + def group return @group if defined?(@group) diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 6584aa3edd5..8231f8fa334 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -180,12 +180,9 @@ module IssuablesHelper end def issuables_count_for_state(issuable_type, state) - issuables_finder = public_send("#{issuable_type}_finder") - - params = issuables_finder.params.merge(state: state) - finder = issuables_finder.class.new(issuables_finder.current_user, params) - - finder.execute.page(1).total_count + @counts ||= {} + @counts[issuable_type] ||= public_send("#{issuable_type}_finder").count_by_state + @counts[issuable_type][state] end IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page] @@ -195,6 +192,7 @@ module IssuablesHelper opts = params.with_indifferent_access opts[:state] = state opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY) + opts.delete_if { |_, value| value.blank? } hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-')) end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index df87fac132d..a3331dc80cb 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -20,6 +20,11 @@ module NavHelper end elsif current_path?('builds#show') "page-gutter build-sidebar right-sidebar-expanded" + elsif current_path?('wikis#show') || + current_path?('wikis#edit') || + current_path?('wikis#history') || + current_path?('wikis#git_access') + "page-gutter wiki-sidebar right-sidebar-expanded" end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index e7d33bd26db..88c46076df6 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -203,7 +203,7 @@ module Ci .reorder(iid: :asc) merge_requests.find do |merge_request| - merge_request.commits.any? { |ci| ci.id == pipeline.sha } + merge_request.commits_sha.include?(pipeline.sha) end end diff --git a/app/models/commit.rb b/app/models/commit.rb index 9e7fde9503d..176c524cf7b 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -48,6 +48,10 @@ class Commit max_lines: DIFF_HARD_LIMIT_LINES, } end + + def from_hash(hash, project) + new(Gitlab::Git::Commit.new(hash), project) + end end attr_accessor :raw diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 64990f8134e..bfb016df46d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -22,7 +22,8 @@ class MergeRequest < ActiveRecord::Base after_create :ensure_merge_request_diff, unless: :importing? after_update :reload_diff_if_branch_changed - delegate :commits, :real_size, to: :merge_request_diff, prefix: nil + delegate :commits, :real_size, :commits_sha, :commits_count, + to: :merge_request_diff, prefix: nil # When this attribute is true some MR validation is ignored # It allows us to close or modify broken merge requests @@ -662,7 +663,7 @@ class MergeRequest < ActiveRecord::Base end def broken? - self.commits.blank? || branch_missing? || cannot_be_merged? + has_no_commits? || branch_missing? || cannot_be_merged? end def can_be_merged_by?(user) @@ -770,10 +771,6 @@ class MergeRequest < ActiveRecord::Base diverged_commits_count > 0 end - def commits_sha - commits.map(&:sha) - end - def head_pipeline return unless diff_head_sha && source_project @@ -871,4 +868,12 @@ class MergeRequest < ActiveRecord::Base @conflicts_can_be_resolved_in_ui = false end end + + def has_commits? + commits_count > 0 + end + + def has_no_commits? + !has_commits? + end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 58a24eb84cb..b8f36a2c958 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -127,11 +127,7 @@ class MergeRequestDiff < ActiveRecord::Base end def commits_sha - if @commits - commits.map(&:sha) - else - st_commits.map { |commit| commit[:id] } - end + st_commits.map { |commit| commit[:id] } end def diff_refs @@ -176,6 +172,10 @@ class MergeRequestDiff < ActiveRecord::Base CompareService.new.execute(project, head_commit_sha, project, sha, straight: straight) end + def commits_count + st_commits.count + end + private # Old GitLab implementations may have generated diffs as ["--broken-diff"]. diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 647930d555c..185556c12cc 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -135,7 +135,7 @@ class GitPushService < BaseService @push_commits.each do |commit| ProcessCommitWorker. - perform_async(project.id, current_user.id, commit.id, default) + perform_async(project.id, current_user.id, commit.to_hash, default) end end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 22596b4014a..e4056306bc4 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -63,7 +63,7 @@ module MergeRequests if merge_request.source_branch == @branch_name || force_push? merge_request.reload_diff else - mr_commit_ids = merge_request.commits.map(&:id) + mr_commit_ids = merge_request.commits_sha push_commit_ids = @commits.map(&:id) matches = mr_commit_ids & push_commit_ids merge_request.reload_diff if matches.any? @@ -123,7 +123,7 @@ module MergeRequests return unless @commits.present? merge_requests_for_source_branch.each do |merge_request| - mr_commit_ids = Set.new(merge_request.commits.map(&:id)) + mr_commit_ids = Set.new(merge_request.commits_sha) new_commits, existing_commits = @commits.partition do |commit| mr_commit_ids.include?(commit.id) diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 2a6d9cda379..817e4bebb05 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,5 +1,4 @@ .nav-sidebar - .sidebar-header Across GitLab %ul.nav = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do diff --git a/app/views/projects/boards/components/sidebar/_notifications.html.haml b/app/views/projects/boards/components/sidebar/_notifications.html.haml index 21c9563e9db..a08c7f2af09 100644 --- a/app/views/projects/boards/components/sidebar/_notifications.html.haml +++ b/app/views/projects/boards/components/sidebar/_notifications.html.haml @@ -1,11 +1,7 @@ - if current_user .block.light.subscription{ ":data-url" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '/toggle_subscription'" } - .title + %span.issuable-header-text.hide-collapsed.pull-left Notifications - %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } - {{ issue.subscribed ? 'Unsubscribe' : 'Subscribe' }} - .subscription-status{ ":data-status" => "issue.subscribed ? 'subscribed' : 'unsubscribed'" } - .unsubscribed{ "v-show" => "!issue.subscribed" } - You're not receiving notifications from this thread. - .subscribed{ "v-show" => "issue.subscribed" } - You're receiving notifications because you're subscribed to this thread. + %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } + %span + {{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}} diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml index eab48b78cb3..5cc92595fe0 100644 --- a/app/views/projects/merge_requests/show/_versions.html.haml +++ b/app/views/projects/merge_requests/show/_versions.html.haml @@ -27,7 +27,7 @@ version #{version_index(merge_request_diff)} .monospace #{short_sha(merge_request_diff.head_commit_sha)} %small - #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)}, + #{number_with_delimiter(merge_request_diff.commits_count)} #{'commit'.pluralize(merge_request_diff.commits_count)}, = time_ago_with_tooltip(merge_request_diff.created_at) - if @merge_request_diff.base_commit_sha diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 20c93930abc..eee711dc5af 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -11,7 +11,7 @@ = render 'projects/merge_requests/widget/open/archived' - elsif @merge_request.branch_missing? = render 'projects/merge_requests/widget/open/missing_branch' - - elsif @merge_request.commits.blank? + - elsif @merge_request.has_no_commits? = render 'projects/merge_requests/widget/open/nothing' - elsif @merge_request.unchecked? = render 'projects/merge_requests/widget/open/check' diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 718314701f9..3464e155a1b 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -1,14 +1,17 @@ .tabs-holder - %ul.nav-links.no-top.no-bottom - %li.active - = link_to "Pipeline", "#js-tab-pipeline", data: { target: '#js-tab-pipeline', action: 'pipeline', toggle: 'tab' }, class: 'pipeline-tab' - %li - = link_to "#js-tab-builds", data: { target: '#js-tab-builds', action: 'build', toggle: 'tab' }, class: 'builds-tab' do + %ul.pipelines-tabs.nav-links.no-top.no-bottom + %li.js-pipeline-tab-link + = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: { target: 'div#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do + Pipeline + %li.js-builds-tab-link + = link_to builds_namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do Builds - %span.badge= pipeline.statuses.count + %span.badge.js-builds-counter= pipeline.statuses.count + + .tab-content - #js-tab-pipeline.tab-pane.active + #js-tab-pipeline.tab-pane .build-content.middle-block.pipeline-graph .pipeline-visualization %ul.stage-column-list diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index 8c6652a5f90..29a41bc664b 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -2,7 +2,7 @@ - page_title "Pipeline" = render "projects/pipelines/head" -%div{ class: container_class } +%div.js-pipeline-container{ class: container_class, data: { controller_action: "#{controller.action_name}" } } - if @commit = render "projects/pipelines/info" diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 4e41a15d9f4..c52527332bc 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -34,9 +34,6 @@ - if @page && @page.persisted? = f.submit 'Save changes', class: "btn-save btn" .pull-right - - if can?(current_user, :admin_wiki, @project) - = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger btn-grouped" do - Delete = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-cancel btn-grouped" - else = f.submit 'Create page', class: "btn-create btn" diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml deleted file mode 100644 index afdef70e1cf..00000000000 --- a/app/views/projects/wikis/_nav.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -= content_for :sub_nav do - .scrolling-tabs-container.sub-nav-scroll - = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs - %ul{ class: (container_class) } - = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do - = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) - - = nav_link(path: 'wikis#pages') do - = link_to 'Pages', namespace_project_wikis_pages_path(@project.namespace, @project) - - = nav_link(path: 'wikis#git_access') do - = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do - Git Access - - = render 'projects/wikis/new' diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml new file mode 100644 index 00000000000..cad9c15a49e --- /dev/null +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -0,0 +1,23 @@ +%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar + .block.wiki-sidebar-header.append-bottom-default + %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" } + = icon('angle-double-right') + + - git_access_url = namespace_project_wikis_git_access_path(@project.namespace, @project) + = link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '' do + = succeed ' ' do + = icon('cloud-download') + Clone repository + + .blocks-container + .block.block-first + %ul.wiki-pages + - @sidebar_wiki_pages.each do |wiki_page| + %li{ class: params[:id] == wiki_page.slug ? 'active' : '' } + = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do + = wiki_page.title.capitalize + .block + = link_to namespace_project_wikis_pages_path(@project.namespace, @project), class: 'btn btn-block' do + More Pages + += render 'projects/wikis/new' diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 679d6018bef..8cf018da1b7 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,23 +1,35 @@ - @no_container = true - page_title "Edit", @page.title.capitalize, "Wiki" -= render 'nav' %div{ class: container_class } - .top-area + .wiki-page-header.has-sidebar-toggle + %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + = icon('angle-double-left') + .nav-text - %strong + %h2.wiki-page-title - if @page.persisted? = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) - else = @page.title.capitalize - %span.light - · - Edit Page + %span.light + · + - if @page.persisted? + Edit Page + - else + Create Page .nav-controls - - if !(@page && @page.persisted?) - - if can?(current_user, :create_wiki, @project) - = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do - New Page + - if can?(current_user, :create_wiki, @project) + = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do + New Page + - if @page.persisted? + = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do + Page History + - if can?(current_user, :admin_wiki, @project) + = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger" do + Delete = render 'form' + += render 'sidebar' diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index b8811a28dd6..e25d6a48573 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -1,34 +1,36 @@ - @no_container = true - page_title "Git Access", "Wiki" -= render 'nav' %div{ class: container_class } - .sub-header-block - %span.oneline - Git access for + .wiki-page-header.has-sidebar-toggle + %button.btn.btn-default.visible-xs.visible-sm.pull-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + = icon('angle-double-left') + + .git-access-header + Clone repository %strong= @project_wiki.path_with_namespace - .pull-right - = render "shared/clone_panel", project: @project_wiki + = render "shared/clone_panel", project: @project_wiki + + .wiki-git-access + %h3 Install Gollum + %pre.dark + :preserve + gem install gollum - .prepend-top-default - %fieldset - %legend Install Gollum: - %pre.dark - :preserve - gem install gollum + %h3 Clone your wiki + %pre.dark + :preserve + git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')} + cd #{h @project_wiki.path} - %legend Clone Your Wiki: - %pre.dark - :preserve - git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')} - cd #{h @project_wiki.path} + %h3 Start Gollum and edit locally + %pre.dark + :preserve + gollum + == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin + >> Thin web server (v1.5.0 codename Knife) + >> Maximum connections set to 1024 + >> Listening on 0.0.0.0:4567, CTRL+C to stop - %legend Start Gollum And Edit Locally: - %pre.dark - :preserve - gollum - == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin - >> Thin web server (v1.5.0 codename Knife) - >> Maximum connections set to 1024 - >> Listening on 0.0.0.0:4567, CTRL+C to stop += render 'sidebar' diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index 4c0b14e2c42..dd7213622c1 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,13 +1,16 @@ - page_title "History", @page.title.capitalize, "Wiki" -= render 'nav' + %div{ class: container_class } - .top-area + .wiki-page-header.has-sidebar-toggle + %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + = icon('angle-double-left') + .nav-text - %strong + %h2.wiki-page-title = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) - %span.light - · - History + %span.light + · + History .table-holder %table.table @@ -35,3 +38,5 @@ %td %strong = @page.page.wiki.page(@page.page.name, commit.id).try(:format) + += render 'sidebar' diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index 9c10acd4cb6..e1eaffc6884 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -1,9 +1,18 @@ - @no_container = true - page_title "Pages", "Wiki" -= render 'nav' - %div{ class: container_class } + .wiki-page-header + + .nav-text + %h2.wiki-page-title + Wiki Pages + + .nav-controls + = link_to namespace_project_wikis_git_access_path(@project.namespace, @project), class: 'btn' do + = icon('cloud-download') + Clone repository + %ul.content-list - @wiki_pages.each do |wiki_page| %li diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 5cebb538cf5..1b6dceee241 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -1,15 +1,19 @@ - @no_container = true - page_title @page.title.capitalize, "Wiki" -= render 'nav' %div{ class: container_class } - .top-area + .wiki-page-header.has-sidebar-toggle + %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + = icon('angle-double-left') + .nav-text - %strong= @page.title.capitalize + %h2.wiki-page-title= @page.title.capitalize %span.wiki-last-edit-by - · - last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} + Last edited by + %strong + #{@page.commit.author.name} + #{time_ago_with_tooltip(@page.commit.authored_date)} .nav-controls = render 'main_links' @@ -19,8 +23,9 @@ This is an old version of this page. You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}. - .wiki-holder.prepend-top-default.append-bottom-default .wiki = preserve do = render_wiki_content(@page) + += render 'sidebar' diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml index baa6d5f8206..26b349e8a62 100644 --- a/app/views/shared/_issues.html.haml +++ b/app/views/shared/_issues.html.haml @@ -1,4 +1,4 @@ -- if @issues.reorder(nil).any? +- if @issues.to_a.any? - @issues.group_by(&:project).each do |group| .panel.panel-default.panel-small - project = group[0] diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml index ca3178395c1..2f3605b4d27 100644 --- a/app/views/shared/_merge_requests.html.haml +++ b/app/views/shared/_merge_requests.html.haml @@ -1,4 +1,4 @@ -- if @merge_requests.reorder(nil).any? +- if @merge_requests.to_a.any? - @merge_requests.group_by(&:target_project).each do |group| .panel.panel-default.panel-small - project = group[0] diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 02427650219..958f8413e1d 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -144,16 +144,11 @@ .block.light.subscription{data: {url: toggle_subscription_path(issuable)}} .sidebar-collapsed-icon = icon('rss') - .title.hide-collapsed + %span.issuable-header-text.hide-collapsed.pull-left Notifications - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' - %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } + %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } %span= subscribed ? 'Unsubscribe' : 'Subscribe' - .subscription-status.hide-collapsed{data: {status: subscribtion_status}} - .unsubscribed{class: ( 'hidden' if subscribed )} - You're not receiving notifications from this thread. - .subscribed{class: ( 'hidden' unless subscribed )} - You're receiving notifications because you're subscribed to this thread. - project_ref = cross_project_reference(@project, issuable) .block.project-reference @@ -170,6 +165,6 @@ new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}'); new LabelsSelect(); new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); - new Subscription('.subscription') + gl.Subscription.bindAll('.subscription'); new gl.DueDateSelectors(); sidebar = new Sidebar(); diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb index 071741fbacd..e9a5bd7f24e 100644 --- a/app/workers/process_commit_worker.rb +++ b/app/workers/process_commit_worker.rb @@ -10,9 +10,10 @@ class ProcessCommitWorker # project_id - The ID of the project this commit belongs to. # user_id - The ID of the user that pushed the commit. - # commit_sha - The SHA1 of the commit to process. + # commit_hash - Hash containing commit details to use for constructing a + # Commit object without having to use the Git repository. # default - The data was pushed to the default branch. - def perform(project_id, user_id, commit_sha, default = false) + def perform(project_id, user_id, commit_hash, default = false) project = Project.find_by(id: project_id) return unless project @@ -21,10 +22,7 @@ class ProcessCommitWorker return unless user - commit = find_commit(project, commit_sha) - - return unless commit - + commit = build_commit(project, commit_hash) author = commit.author || user process_commit_message(project, commit, user, author, default) @@ -59,9 +57,18 @@ class ProcessCommitWorker update_all(first_mentioned_in_commit_at: commit.committed_date) end - private + def build_commit(project, hash) + date_suffix = '_date' + + # When processing Sidekiq payloads various timestamps are stored as Strings. + # Commit in turn expects Time-like instances upon input, so we have to + # manually parse these values. + hash.each do |key, value| + if key.to_s.end_with?(date_suffix) && value.is_a?(String) + hash[key] = Time.parse(value) + end + end - def find_commit(project, sha) - project.commit(sha) + Commit.from_hash(hash, project) end end diff --git a/changelogs/unreleased/18546-update-wiki-page-design.yml b/changelogs/unreleased/18546-update-wiki-page-design.yml new file mode 100644 index 00000000000..c76e17340f2 --- /dev/null +++ b/changelogs/unreleased/18546-update-wiki-page-design.yml @@ -0,0 +1,4 @@ +--- +title: Update wiki page design +merge_request: 7429 +author: diff --git a/changelogs/unreleased/22781-user-generated-permalinks.yml b/changelogs/unreleased/22781-user-generated-permalinks.yml new file mode 100644 index 00000000000..e46739e48e3 --- /dev/null +++ b/changelogs/unreleased/22781-user-generated-permalinks.yml @@ -0,0 +1,4 @@ +--- +title: Prevent DOM ID collisions resulting from user-generated content anchors +merge_request: 7631 +author: diff --git a/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml b/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml new file mode 100644 index 00000000000..2227c81bd34 --- /dev/null +++ b/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml @@ -0,0 +1,4 @@ +--- +title: Remove the help text under the sidebar subscribe button and style it inline +merge_request: 7389 +author: diff --git a/changelogs/unreleased/24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml b/changelogs/unreleased/24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml new file mode 100644 index 00000000000..01b19a47ecd --- /dev/null +++ b/changelogs/unreleased/24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml @@ -0,0 +1,4 @@ +--- +title: Speed up issuable dashboards +merge_request: +author: diff --git a/changelogs/unreleased/24726-remove-across-gitlab.yml b/changelogs/unreleased/24726-remove-across-gitlab.yml new file mode 100644 index 00000000000..6436e4b688f --- /dev/null +++ b/changelogs/unreleased/24726-remove-across-gitlab.yml @@ -0,0 +1,4 @@ +--- +title: 24726 Remove Across GitLab from side navigation +merge_request: +author: diff --git a/changelogs/unreleased/24813-project-members-with-developer-access-can-no-longer-create-tags.yml b/changelogs/unreleased/24813-project-members-with-developer-access-can-no-longer-create-tags.yml deleted file mode 100644 index 9254db40742..00000000000 --- a/changelogs/unreleased/24813-project-members-with-developer-access-can-no-longer-create-tags.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Pass tag SHA to post-receive hook when tag is created via UI -merge_request: 7700 -author: diff --git a/changelogs/unreleased/24814-pipeline-tabs.yml b/changelogs/unreleased/24814-pipeline-tabs.yml new file mode 100644 index 00000000000..f85e7576905 --- /dev/null +++ b/changelogs/unreleased/24814-pipeline-tabs.yml @@ -0,0 +1,4 @@ +--- +title: Fix Cicking on tabs on pipeline page should set URL +merge_request: 7709 +author: diff --git a/changelogs/unreleased/24860-actionview-template-error-undefined-method-size-for-nil-nilclass.yml b/changelogs/unreleased/24860-actionview-template-error-undefined-method-size-for-nil-nilclass.yml deleted file mode 100644 index 4b4aea79380..00000000000 --- a/changelogs/unreleased/24860-actionview-template-error-undefined-method-size-for-nil-nilclass.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Prevent error when submitting a merge request and pipeline is not defined -merge_request: 7707 -author: diff --git a/changelogs/unreleased/24894-style-system-note-in-commit-discussion.yml b/changelogs/unreleased/24894-style-system-note-in-commit-discussion.yml deleted file mode 100644 index 7ddf0b46d4c..00000000000 --- a/changelogs/unreleased/24894-style-system-note-in-commit-discussion.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixes system note style in commit discussion -merge_request: 7721 -author: diff --git a/changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml b/changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml deleted file mode 100644 index dad9db0ffef..00000000000 --- a/changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix pipelines info being hidden in merge request widget -merge_request: 7808 -author: diff --git a/changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml b/changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml new file mode 100644 index 00000000000..58efd9113f2 --- /dev/null +++ b/changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml @@ -0,0 +1,4 @@ +--- +title: Don't change relative URLs to absolute URLs in the Help page +merge_request: +author: diff --git a/changelogs/unreleased/4269-public-api.yml b/changelogs/unreleased/4269-public-api.yml new file mode 100644 index 00000000000..560bc6a4f13 --- /dev/null +++ b/changelogs/unreleased/4269-public-api.yml @@ -0,0 +1,4 @@ +--- +title: Allow public access to some Project API endpoints +merge_request: 7843 +author: diff --git a/changelogs/unreleased/boards-issue-sorting.yml b/changelogs/unreleased/boards-issue-sorting.yml deleted file mode 100644 index fb7dc2f9190..00000000000 --- a/changelogs/unreleased/boards-issue-sorting.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed issue boards issue sorting when dragging issue into list -merge_request: -author: diff --git a/changelogs/unreleased/clean-up-jira-service.yml b/changelogs/unreleased/clean-up-jira-service.yml deleted file mode 100644 index a309cb57532..00000000000 --- a/changelogs/unreleased/clean-up-jira-service.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refactor JiraService by moving code out of JiraService#execute method -merge_request: 7756 -author: diff --git a/changelogs/unreleased/comments-fixture.yml b/changelogs/unreleased/comments-fixture.yml new file mode 100644 index 00000000000..824c1c88a60 --- /dev/null +++ b/changelogs/unreleased/comments-fixture.yml @@ -0,0 +1,4 @@ +--- +title: Replace static fixture for notes_spec +merge_request: 7683 +author: winniehell diff --git a/changelogs/unreleased/events-cache-invalidation.yml b/changelogs/unreleased/events-cache-invalidation.yml deleted file mode 100644 index 2b30f4dcbce..00000000000 --- a/changelogs/unreleased/events-cache-invalidation.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove caching of events data -merge_request: 6578 -author: diff --git a/changelogs/unreleased/fix-ca-no-date.yml b/changelogs/unreleased/fix-ca-no-date.yml deleted file mode 100644 index 6de4a56ac0d..00000000000 --- a/changelogs/unreleased/fix-ca-no-date.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix for error thrown in cycle analytics events if build has not started -merge_request: -author: diff --git a/changelogs/unreleased/fix-git-access-wiki-when-repository-feature-disabled.yml b/changelogs/unreleased/fix-git-access-wiki-when-repository-feature-disabled.yml deleted file mode 100644 index 82ca6316876..00000000000 --- a/changelogs/unreleased/fix-git-access-wiki-when-repository-feature-disabled.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow access to the wiki with git when repository feature disabled -merge_request: -author: diff --git a/changelogs/unreleased/fix-github-branch-formatter.yml b/changelogs/unreleased/fix-github-branch-formatter.yml new file mode 100644 index 00000000000..c8698f507de --- /dev/null +++ b/changelogs/unreleased/fix-github-branch-formatter.yml @@ -0,0 +1,4 @@ +--- +title: Fix branch validation for GitHub PR where repo/fork was renamed/deleted +merge_request: +author: diff --git a/changelogs/unreleased/fixed-commit-timeago.yml b/changelogs/unreleased/fixed-commit-timeago.yml deleted file mode 100644 index 295d8db63d0..00000000000 --- a/changelogs/unreleased/fixed-commit-timeago.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed commit timeago not rendering after initial page -merge_request: -author: diff --git a/changelogs/unreleased/process-commit-worker-improvements.yml b/changelogs/unreleased/process-commit-worker-improvements.yml new file mode 100644 index 00000000000..0038c6e34e6 --- /dev/null +++ b/changelogs/unreleased/process-commit-worker-improvements.yml @@ -0,0 +1,4 @@ +--- +title: Pass commit data to ProcessCommitWorker to reduce Git overhead +merge_request: 7744 +author: diff --git a/changelogs/unreleased/refresh-authorizations-with-lease.yml b/changelogs/unreleased/refresh-authorizations-with-lease.yml deleted file mode 100644 index bb9b77018e3..00000000000 --- a/changelogs/unreleased/refresh-authorizations-with-lease.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use a Redis lease for updating authorized projects -merge_request: 7733 -author: diff --git a/changelogs/unreleased/rephrase-system-notes.yml b/changelogs/unreleased/rephrase-system-notes.yml deleted file mode 100644 index e77c3a31cb4..00000000000 --- a/changelogs/unreleased/rephrase-system-notes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Rephrase some system notes to be compatible with new system note style -merge_request: 7692 -author: diff --git a/changelogs/unreleased/resolve-discussions-timeago.yml b/changelogs/unreleased/resolve-discussions-timeago.yml deleted file mode 100644 index ffedeb93f1d..00000000000 --- a/changelogs/unreleased/resolve-discussions-timeago.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed timeago not rendering when resolving a discussion -merge_request: -author: diff --git a/changelogs/unreleased/right-sidebar-fixture.yml b/changelogs/unreleased/right-sidebar-fixture.yml new file mode 100644 index 00000000000..46a3e459fef --- /dev/null +++ b/changelogs/unreleased/right-sidebar-fixture.yml @@ -0,0 +1,4 @@ +--- +title: Replace static fixture for right_sidebar_spec +merge_request: 7687 +author: winniehell diff --git a/changelogs/unreleased/sh-update-sidekiq-cron.yml b/changelogs/unreleased/sh-update-sidekiq-cron.yml deleted file mode 100644 index d79ba817a18..00000000000 --- a/changelogs/unreleased/sh-update-sidekiq-cron.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update Sidekiq-cron to fix compatibility issues with Sidekiq 4.2.1 -merge_request: -author: diff --git a/changelogs/unreleased/shortcuts-issuable-fixture.yml b/changelogs/unreleased/shortcuts-issuable-fixture.yml new file mode 100644 index 00000000000..88945600886 --- /dev/null +++ b/changelogs/unreleased/shortcuts-issuable-fixture.yml @@ -0,0 +1,4 @@ +--- +title: Replace static fixture for shortcuts_issuable_spec +merge_request: 7685 +author: winniehell diff --git a/changelogs/unreleased/timeout-merge-request-for-binary-file.yml b/changelogs/unreleased/timeout-merge-request-for-binary-file.yml deleted file mode 100644 index 5161265d1bd..00000000000 --- a/changelogs/unreleased/timeout-merge-request-for-binary-file.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Timeout creating and viewing merge request for binary file -merge_request: -author: diff --git a/changelogs/unreleased/use-st-commits-where-possible.yml b/changelogs/unreleased/use-st-commits-where-possible.yml new file mode 100644 index 00000000000..e4395461560 --- /dev/null +++ b/changelogs/unreleased/use-st-commits-where-possible.yml @@ -0,0 +1,5 @@ +--- +title: Replace references to MergeRequestDiff#commits with st_commits when we care + only about the number of commits +merge_request: 7668 +author: diff --git a/changelogs/unreleased/workhorse-v1-0-1.yml b/changelogs/unreleased/workhorse-v1-0-1.yml deleted file mode 100644 index c26c2d45b1d..00000000000 --- a/changelogs/unreleased/workhorse-v1-0-1.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update GitLab Workhorse to v1.0.1 -merge_request: 7759 -author: diff --git a/config/initializers/ar_monkey_patch.rb b/config/initializers/ar_monkey_patch.rb index 0da584626ee..6979f4641b0 100644 --- a/config/initializers/ar_monkey_patch.rb +++ b/config/initializers/ar_monkey_patch.rb @@ -52,6 +52,23 @@ module ActiveRecord raise end end + + # This is patched because we need it to query `lock_version IS NULL` + # rather than `lock_version = 0` whenever lock_version is NULL. + def relation_for_destroy + return super unless locking_enabled? + + column_name = self.class.locking_column + super.where(self.class.arel_table[column_name].eq(self[column_name])) + end + end + + # This is patched because we want `lock_version` default to `NULL` + # rather than `0` + class LockingType < SimpleDelegator + def type_cast_from_database(value) + super + end end end end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index b87b31d9697..1d7a3f03ace 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -61,5 +61,5 @@ begin end end end -rescue Redis::BaseError, SocketError +rescue Redis::BaseError, SocketError, Errno::ENOENT, Errno::EAFNOSUPPORT, Errno::ECONNRESET, Errno::ECONNREFUSED end diff --git a/config/routes/project.rb b/config/routes/project.rb index 1336484a399..0754f0ec3b0 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -129,6 +129,7 @@ constraints(ProjectUrlConstrainer.new) do member do post :cancel post :retry + get :builds end end diff --git a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb new file mode 100644 index 00000000000..453a44e271a --- /dev/null +++ b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb @@ -0,0 +1,92 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + class Project < ActiveRecord::Base + def self.find_including_path(id) + select("projects.*, CONCAT(namespaces.path, '/', projects.path) AS path_with_namespace"). + joins('INNER JOIN namespaces ON namespaces.id = projects.namespace_id'). + find_by(id: id) + end + + def repository_storage_path + Gitlab.config.repositories.storages[repository_storage] + end + + def repository_path + File.join(repository_storage_path, read_attribute(:path_with_namespace) + '.git') + end + + def repository + @repository ||= Rugged::Repository.new(repository_path) + end + end + + DOWNTIME = true + DOWNTIME_REASON = 'Existing workers will error until they are using a newer version of the code' + + disable_ddl_transaction! + + def up + Sidekiq.redis do |redis| + new_jobs = [] + + while job = redis.lpop('queue:process_commit') + payload = JSON.load(job) + project = Project.find_including_path(payload['args'][0]) + + next unless project + + begin + commit = project.repository.lookup(payload['args'][2]) + rescue Rugged::OdbError + next + end + + hash = { + id: commit.oid, + message: commit.message, + parent_ids: commit.parent_ids, + authored_date: commit.author[:time], + author_name: commit.author[:name], + author_email: commit.author[:email], + committed_date: commit.committer[:time], + committer_email: commit.committer[:email], + committer_name: commit.committer[:name] + } + + payload['args'][2] = hash + + new_jobs << JSON.dump(payload) + end + + redis.multi do |multi| + new_jobs.each do |j| + multi.lpush('queue:process_commit', j) + end + end + end + end + + def down + Sidekiq.redis do |redis| + new_jobs = [] + + while job = redis.lpop('queue:process_commit') + payload = JSON.load(job) + + payload['args'][2] = payload['args'][2]['id'] + + new_jobs << JSON.dump(payload) + end + + redis.multi do |multi| + new_jobs.each do |j| + multi.lpush('queue:process_commit', j) + end + end + end + end +end diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md index 7bfc9cb361f..0f78e8238af 100644 --- a/doc/development/gotchas.md +++ b/doc/development/gotchas.md @@ -141,51 +141,3 @@ in an initializer._ ### Further reading - Stack Overflow: [Why you should not write inline JavaScript](http://programmers.stackexchange.com/questions/86589/why-should-i-avoid-inline-scripting) - -## ID-based CSS selectors need to be a bit more specific - -Normally, because HTML `id` attributes need to be unique to the page, it's -perfectly fine to write some JavaScript like the following: - -```javascript -$('#js-my-selector').hide(); -``` - -However, there's a feature of GitLab's Markdown processing that [automatically -adds anchors to header elements][ToC Processing], with the `id` attribute being -automatically generated based on the content of the header. - -Unfortunately, this feature makes it possible for user-generated content to -create a header element with the same `id` attribute we're using in our -selector, potentially breaking the JavaScript behavior. A user could break the -above example with the following Markdown: - -```markdown -## JS My Selector -``` - -Which gets converted to the following HTML: - -```html -<h2> - <a id="js-my-selector" class="anchor" href="#js-my-selector" aria-hidden="true"></a> - JS My Selector -</h2> -``` - -[ToC Processing]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/lib/banzai/filter/table_of_contents_filter.rb#L31-37 - -### Solution - -The current recommended fix for this is to make our selectors slightly more -specific: - -```javascript -$('div#js-my-selector').hide(); -``` - -### Further reading - -- Issue: [Merge request ToC anchor conflicts with tabs](https://gitlab.com/gitlab-org/gitlab-ce/issues/3908) -- Merge Request: [Make tab target selectors less naive](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2023) -- Merge Request: [Make cross-project reference's clipboard target less naive](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2024) diff --git a/features/project/wiki.feature b/features/project/wiki.feature index 63ce3ccb536..a04228de03b 100644 --- a/features/project/wiki.feature +++ b/features/project/wiki.feature @@ -49,7 +49,6 @@ Feature: Project Wiki Scenario: View all pages Given I have an existing wiki page And I browse to that Wiki page - And I click on the "Pages" button Then I should see the existing page in the pages list Scenario: File exists in wiki repo @@ -72,13 +71,11 @@ Feature: Project Wiki @javascript Scenario: New Wiki page that has a path Given I create a New page with paths - And I click on the "Pages" button Then I should see non-escaped link in the pages list @javascript Scenario: Edit Wiki page that has a path Given I create a New page with paths - And I click on the "Pages" button And I edit the Wiki page with a path Then I should see a non-escaped path And I should see the Editing page @@ -88,7 +85,6 @@ Feature: Project Wiki @javascript Scenario: View the page history of a Wiki page that has a path Given I create a New page with paths - And I click on the "Pages" button And I view the page history of a Wiki page that has a path Then I should see a non-escaped path And I should see the page history @@ -96,7 +92,6 @@ Feature: Project Wiki @javascript Scenario: View an old page version of a Wiki page Given I create a New page with paths - And I click on the "Pages" button And I edit the Wiki page with a path Then I should see a non-escaped path And I should see the Editing page diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb index 2134dae168a..dee6a8a5558 100644 --- a/features/steps/project/source/markdown_render.rb +++ b/features/steps/project/source/markdown_render.rb @@ -241,7 +241,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps page.within(:css, ".nav-text") do expect(page).to have_content "Test" - expect(page).to have_content "Edit Page" + expect(page).to have_content "Create Page" end end @@ -258,7 +258,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "api") page.within(:css, ".nav-text") do - expect(page).to have_content "Edit" + expect(page).to have_content "Create" expect(page).to have_content "Api" end end @@ -271,7 +271,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "raketasks") page.within(:css, ".nav-text") do - expect(page).to have_content "Edit" + expect(page).to have_content "Create" expect(page).to have_content "Rake" end end diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index 07a955b1a14..4cb0a21fbb4 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -29,7 +29,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps expect(page).to have_content "link test" click_link "link test" - expect(page).to have_content "Edit Page" + expect(page).to have_content "Create Page" end step 'I have an existing Wiki page' do @@ -80,13 +80,9 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps expect(page).to have_content "Page was successfully deleted" end - step 'I click on the "Pages" button' do - click_on "Pages" - end - step 'I should see the existing page in the pages list' do expect(page).to have_content current_user.name - expect(page).to have_content @page.title + expect(find('.wiki-pages')).to have_content @page.title.capitalize end step 'I have an existing Wiki page with images linked on page' do @@ -125,7 +121,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps step 'I should see the new wiki page form' do expect(current_path).to match('wikis/image.jpg') expect(page).to have_content('New Wiki Page') - expect(page).to have_content('Edit Page') + expect(page).to have_content('Create Page') end step 'I create a New page with paths' do @@ -142,8 +138,8 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I edit the Wiki page with a path' do - expect(page).to have_content('three') - click_on 'three' + expect(find('.wiki-pages')).to have_content('Three') + click_on 'Three' expect(find('.nav-text')).to have_content('Three') click_on 'Edit' end @@ -157,7 +153,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I view the page history of a Wiki page that has a path' do - click_on 'three' + click_on 'Three' click_on 'Page History' end diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb index 56b36f7c46c..a036d9b884f 100644 --- a/features/steps/shared/markdown.rb +++ b/features/steps/shared/markdown.rb @@ -2,7 +2,7 @@ module SharedMarkdown include Spinach::DSL def header_should_have_correct_id_and_link(level, text, id, parent = ".wiki") - node = find("#{parent} h#{level} a##{id}") + node = find("#{parent} h#{level} a#user-content-#{id}") expect(node[:href]).to eq "##{id}" # Work around a weird Capybara behavior where calling `parent` on a node diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index cbafa952ef6..7f94ede7940 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -141,6 +141,10 @@ module API unauthorized! unless current_user end + def authenticate_non_get! + authenticate! unless %w[GET HEAD].include?(route.route_method) + end + def authenticate_by_gitlab_shell_token! input = params['secret_token'].try(:chomp) unless Devise.secure_compare(secret_token, input) @@ -149,6 +153,7 @@ module API end def authenticated_as_admin! + authenticate! forbidden! unless current_user.is_admin? end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 8975b1a751c..2929d2157dc 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -3,7 +3,7 @@ module API class Projects < Grape::API include PaginationParams - before { authenticate! } + before { authenticate_non_get! } helpers do params :optional_params do @@ -61,7 +61,7 @@ module API end end - desc 'Get a projects list for authenticated user' do + desc 'Get a list of visible projects for authenticated user' do success Entities::BasicProjectDetails end params do @@ -70,15 +70,15 @@ module API use :filter_params use :pagination end - get do - projects = current_user.authorized_projects + get '/visible' do + projects = ProjectsFinder.new.execute(current_user) projects = filter_projects(projects) - entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess + entity = params[:simple] || !current_user ? Entities::BasicProjectDetails : Entities::ProjectWithAccess present paginate(projects), with: entity, user: current_user end - desc 'Get a list of visible projects for authenticated user' do + desc 'Get a projects list for authenticated user' do success Entities::BasicProjectDetails end params do @@ -87,8 +87,10 @@ module API use :filter_params use :pagination end - get '/visible' do - projects = ProjectsFinder.new.execute(current_user) + get do + authenticate! + + projects = current_user.authorized_projects projects = filter_projects(projects) entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess @@ -103,6 +105,8 @@ module API use :pagination end get '/owned' do + authenticate! + projects = current_user.owned_projects projects = filter_projects(projects) @@ -117,6 +121,8 @@ module API use :pagination end get '/starred' do + authenticate! + projects = current_user.viewable_starred_projects projects = filter_projects(projects) @@ -132,6 +138,7 @@ module API end get '/all' do authenticated_as_admin! + projects = Project.all projects = filter_projects(projects) @@ -213,7 +220,8 @@ module API success Entities::ProjectWithAccess end get ":id" do - present user_project, with: Entities::ProjectWithAccess, user: current_user, + entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails + present user_project, with: entity, user: current_user, user_can_admin_project: can?(current_user, :admin_project, user_project) end @@ -433,7 +441,7 @@ module API use :pagination end get ':id/users' do - users = User.where(id: user_project.team.users.map(&:id)) + users = user_project.team.users users = users.search(params[:search]) if params[:search].present? present paginate(users), with: Entities::UserBasic diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb index a4eda6fdf76..8e7084f2543 100644 --- a/lib/banzai/filter/table_of_contents_filter.rb +++ b/lib/banzai/filter/table_of_contents_filter.rb @@ -35,9 +35,11 @@ module Banzai headers[id] += 1 if header_content = node.children.first + # namespace detection will be automatically handled via javascript (see issue #22781) + namespace = "user-content-" href = "#{id}#{uniq}" push_toc(href, text) - header_content.add_previous_sibling(anchor_tag(href)) + header_content.add_previous_sibling(anchor_tag("#{namespace}#{href}", href)) end end @@ -48,8 +50,8 @@ module Banzai private - def anchor_tag(href) - %Q{<a id="#{href}" class="anchor" href="##{href}" aria-hidden="true"></a>} + def anchor_tag(id, href) + %Q{<a id="#{id}" class="anchor" href="##{href}" aria-hidden="true"></a>} end def push_toc(href, text) diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb index 4750675ae9d..0a8d05b5fe1 100644 --- a/lib/gitlab/github_import/branch_formatter.rb +++ b/lib/gitlab/github_import/branch_formatter.rb @@ -8,7 +8,7 @@ module Gitlab end def valid? - repo.present? + sha.present? && ref.present? end private diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb index cffed987f6b..d3489324a9c 100644 --- a/spec/controllers/help_controller_spec.rb +++ b/spec/controllers/help_controller_spec.rb @@ -8,26 +8,32 @@ describe HelpController do end describe 'GET #index' do - context 'when url prefixed without /help/' do - it 'has correct url prefix' do - stub_readme("[API](api/README.md)") + context 'with absolute url' do + it 'keeps the URL absolute' do + stub_readme("[API](/api/README.md)") + get :index - expect(assigns[:help_index]).to eq '[API](/help/api/README.md)' + + expect(assigns[:help_index]).to eq '[API](/api/README.md)' end end - context 'when url prefixed with help' do - it 'will be an absolute path' do - stub_readme("[API](helpful_hints/README.md)") + context 'with relative url' do + it 'prefixes it with /help/' do + stub_readme("[API](api/README.md)") + get :index - expect(assigns[:help_index]).to eq '[API](/help/helpful_hints/README.md)' + + expect(assigns[:help_index]).to eq '[API](/help/api/README.md)' end end context 'when url is an external link' do - it 'will not be changed' do + it 'does not change it' do stub_readme("[external](https://some.external.link)") + get :index + expect(assigns[:help_index]).to eq '[external](https://some.external.link)' end end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index f160052a844..c16aafa1470 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -304,8 +304,8 @@ describe 'Issue Boards', feature: true, js: true do page.within('.subscription') do click_button 'Subscribe' - - expect(page).to have_content("You're receiving notifications because you're subscribed to this thread.") + wait_for_ajax + expect(page).to have_content("Unsubscribe") end end end diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb index 73d03837144..4319d6db0d2 100644 --- a/spec/features/help_pages_spec.rb +++ b/spec/features/help_pages_spec.rb @@ -12,9 +12,9 @@ describe 'Help Pages', feature: true do end describe 'Get the main help page' do - shared_examples_for 'help page' do + shared_examples_for 'help page' do |prefix: ''| it 'prefixes links correctly' do - expect(page).to have_selector('div.documentation-index > ul a[href="/help/api/README.md"]') + expect(page).to have_selector(%(div.documentation-index > ul a[href="#{prefix}/help/api/README.md"])) end end @@ -33,5 +33,14 @@ describe 'Help Pages', feature: true do it_behaves_like 'help page' end + + context 'with a relative installation' do + before do + stub_config_setting(relative_url_root: '/gitlab') + visit help_path + end + + it_behaves_like 'help page', prefix: '/gitlab' + end end end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb new file mode 100644 index 00000000000..3350a3aeefc --- /dev/null +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -0,0 +1,154 @@ +require 'spec_helper' + +describe "Pipelines", feature: true, js: true do + include GitlabRoutingHelper + + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + before do + login_as(user) + project.team << [user, :developer] + end + + describe 'GET /:project/pipelines/:id' do + let(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) } + + before do + @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') + @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') + @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') + @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build') + @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') + end + + before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } + + it 'shows the pipeline graph' do + expect(page).to have_selector('.pipeline-visualization') + expect(page).to have_content('Build') + expect(page).to have_content('Test') + expect(page).to have_content('Deploy') + expect(page).to have_content('Retry failed') + expect(page).to have_content('Cancel running') + end + + it 'shows Pipeline tab pane as active' do + expect(page).to have_css('#js-tab-pipeline.active') + end + + context 'page tabs' do + it 'shows Pipeline and Builds tabs with link' do + expect(page).to have_link('Pipeline') + expect(page).to have_link('Builds') + end + + it 'shows counter in Builds tab' do + expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s) + end + + it 'shows Pipeline tab as active' do + expect(page).to have_css('.js-pipeline-tab-link.active') + end + end + + context 'retrying builds' do + it { expect(page).not_to have_content('retried') } + + context 'when retrying' do + before { click_on 'Retry failed' } + + it { expect(page).not_to have_content('Retry failed') } + end + end + + context 'canceling builds' do + it { expect(page).not_to have_selector('.ci-canceled') } + + context 'when canceling' do + before { click_on 'Cancel running' } + + it { expect(page).not_to have_content('Cancel running') } + end + end + end + + describe 'GET /:project/pipelines/:id/builds' do + let(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) } + + before do + @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') + @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') + @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') + @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build') + @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') + end + + before { visit builds_namespace_project_pipeline_path(project.namespace, project, pipeline)} + + it 'shows a list of builds' do + expect(page).to have_content('Test') + expect(page).to have_content(@success.id) + expect(page).to have_content('Deploy') + expect(page).to have_content(@failed.id) + expect(page).to have_content(@running.id) + expect(page).to have_content(@external.id) + expect(page).to have_content('Retry failed') + expect(page).to have_content('Cancel running') + expect(page).to have_link('Play') + end + + it 'shows Builds tab pane as active' do + expect(page).to have_css('#js-tab-builds.active') + end + + context 'page tabs' do + it 'shows Pipeline and Builds tabs with link' do + expect(page).to have_link('Pipeline') + expect(page).to have_link('Builds') + end + + it 'shows counter in Builds tab' do + expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s) + end + + it 'shows Builds tab as active' do + expect(page).to have_css('li.js-builds-tab-link.active') + end + end + + context 'retrying builds' do + it { expect(page).not_to have_content('retried') } + + context 'when retrying' do + before { click_on 'Retry failed' } + + it { expect(page).not_to have_content('Retry failed') } + it { expect(page).to have_selector('.retried') } + end + end + + context 'canceling builds' do + it { expect(page).not_to have_selector('.ci-canceled') } + + context 'when canceling' do + before { click_on 'Cancel running' } + + it { expect(page).not_to have_content('Cancel running') } + it { expect(page).to have_selector('.ci-canceled') } + end + end + + context 'playing manual build' do + before do + within '.pipeline-holder' do + click_link('Play') + end + end + + it { expect(@manual.reload).to be_pending } + end + end +end diff --git a/spec/features/projects/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 10e5466fc85..f3731698a18 100644 --- a/spec/features/projects/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -152,65 +152,6 @@ describe "Pipelines" do end end - describe 'GET /:project/pipelines/:id' do - let(:project) { create(:project) } - let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) } - - before do - @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') - @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') - @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') - @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build') - @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') - end - - before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } - - it 'shows a list of builds' do - expect(page).to have_content('Test') - expect(page).to have_content(@success.id) - expect(page).to have_content('Deploy') - expect(page).to have_content(@failed.id) - expect(page).to have_content(@running.id) - expect(page).to have_content(@external.id) - expect(page).to have_content('Retry failed') - expect(page).to have_content('Cancel running') - expect(page).to have_link('Play') - end - - context 'retrying builds' do - it { expect(page).not_to have_content('retried') } - - context 'when retrying' do - before { click_on 'Retry failed' } - - it { expect(page).not_to have_content('Retry failed') } - it { expect(page).to have_selector('.retried') } - end - end - - context 'canceling builds' do - it { expect(page).not_to have_selector('.ci-canceled') } - - context 'when canceling' do - before { click_on 'Cancel running' } - - it { expect(page).not_to have_content('Cancel running') } - it { expect(page).to have_selector('.ci-canceled') } - end - end - - context 'playing manual build' do - before do - within '.pipeline-holder' do - click_link('Play') - end - end - - it { expect(@manual.reload).to be_pending } - end - end - describe 'POST /:project/pipelines' do let(:project) { create(:project) } diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index 7afd83b7250..fff8b9f3447 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -20,7 +20,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do click_button 'Create page' expect(page).to have_content('Home') - expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end end @@ -41,7 +41,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do click_button 'Create page' expect(page).to have_content('Foo') - expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end @@ -55,7 +55,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do click_button 'Create page' expect(page).to have_content('Spaces in the name') - expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end @@ -69,7 +69,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do click_button 'Create page' expect(page).to have_content('Hyphens in the name') - expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end end @@ -85,7 +85,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do click_button 'Create page' expect(page).to have_content('Home') - expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end end @@ -105,7 +105,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do click_button 'Create page' expect(page).to have_content('Foo') - expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end end diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb index ef82d2375dd..f842d14fa96 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -22,7 +22,7 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do click_button 'Save changes' expect(page).to have_content('Home') - expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end end @@ -37,7 +37,7 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do click_button 'Save changes' expect(page).to have_content('Home') - expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end end diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 new file mode 100644 index 00000000000..9aa3c50611d --- /dev/null +++ b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 @@ -0,0 +1,55 @@ +//= require lib/utils/bootstrap_linked_tabs + +(() => { + describe('Linked Tabs', () => { + fixture.preload('linked_tabs'); + + beforeEach(() => { + fixture.load('linked_tabs'); + }); + + describe('when is initialized', () => { + it('should activate the tab correspondent to the given action', () => { + const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line + action: 'tab1', + defaultAction: 'tab1', + parentEl: '.linked-tabs', + }); + + expect(document.querySelector('#tab1').classList).toContain('active'); + }); + + it('should active the default tab action when the action is show', () => { + const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line + action: 'show', + defaultAction: 'tab1', + parentEl: '.linked-tabs', + }); + + expect(document.querySelector('#tab1').classList).toContain('active'); + }); + }); + + describe('on click', () => { + it('should change the url according to the clicked tab', () => { + const historySpy = spyOn(history, 'replaceState').and.callFake(() => {}); + + const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line + action: 'show', + defaultAction: 'tab1', + parentEl: '.linked-tabs', + }); + + const secondTab = document.querySelector('.linked-tabs li:nth-child(2) a'); + const newState = secondTab.getAttribute('href') + linkedTabs.currentLocation.search + linkedTabs.currentLocation.hash; + + secondTab.click(); + + expect(historySpy).toHaveBeenCalledWith({ + turbolinks: true, + url: newState, + }, document.title, newState); + }); + }); + }); +})(); diff --git a/spec/javascripts/fixtures/comments.html.haml b/spec/javascripts/fixtures/comments.html.haml deleted file mode 100644 index cc1f8f15c21..00000000000 --- a/spec/javascripts/fixtures/comments.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -.flash-container.timeline-content -.timeline-icon.hidden-xs.hidden-sm - %a.author_link - %img -.timeline-content.timeline-content-form - %form.new-note.js-quick-submit.common-note-form.gfm-form.js-main-target-form - .md-area - .md-header - .md-write-holder - .zen-backdrop.div-dropzone-wrapper - .div-dropzone-wrapper - .div-dropzone.dz-clickable - %textarea.note-textarea.js-note-text.js-gfm-input.js-autosize.markdown-area - .note-form-actions.clearfix - %input.btn.btn-nr.btn-create.append-right-10.comment-btn.js-comment-button{ type: 'submit' } - %a.btn.btn-nr.btn-reopen.btn-comment.js-note-target-reopen - Reopen issue - %a.btn.btn-nr.btn-close.btn-comment.js-note-target-close - Close issue - %a.btn.btn-cancel.js-note-discard - Discard draft
\ No newline at end of file diff --git a/spec/javascripts/fixtures/issuable.html.haml b/spec/javascripts/fixtures/issuable.html.haml deleted file mode 100644 index 42ab4aa68b1..00000000000 --- a/spec/javascripts/fixtures/issuable.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -%form.js-main-target-form - %textarea#note_note diff --git a/spec/javascripts/fixtures/issue_note.html.haml b/spec/javascripts/fixtures/issue_note.html.haml deleted file mode 100644 index 0aecc7334fd..00000000000 --- a/spec/javascripts/fixtures/issue_note.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -%ul - %li.note - .js-task-list-container - .note-text - %ul.task-list - %li.task-list-item - %input.task-list-item-checkbox{type: 'checkbox'} - Task List Item - .note-edit-form - %form - %textarea.js-task-list-field - \- [ ] Task List Item diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb index c10784fe5ae..06f708f9e15 100644 --- a/spec/javascripts/fixtures/issues.rb +++ b/spec/javascripts/fixtures/issues.rb @@ -26,8 +26,13 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller end it 'issues/issue-with-task-list.html.raw' do |example| + issue = create(:issue, project: project, description: '- [ ] Task List Item') + render_issue(example.description, issue) + end + + it 'issues/issue_with_comment.html.raw' do |example| issue = create(:issue, project: project) - issue.update(description: '- [ ] Task List Item') + create(:note, project: project, noteable: issue, note: '- [ ] Task List Item').save render_issue(example.description, issue) end diff --git a/spec/javascripts/fixtures/linked_tabs.html.haml b/spec/javascripts/fixtures/linked_tabs.html.haml new file mode 100644 index 00000000000..93c0cf97ff0 --- /dev/null +++ b/spec/javascripts/fixtures/linked_tabs.html.haml @@ -0,0 +1,13 @@ +%ul.nav.nav-tabs.linked-tabs + %li + %a{ href: 'foo/bar/1', data: { target: 'div#tab1', action: 'tab1', toggle: 'tab' } } + Tab 1 + %li + %a{ href: 'foo/bar/1/context', data: { target: 'div#tab2', action: 'tab2', toggle: 'tab' } } + Tab 2 + +.tab-content + #tab1.tab-pane + Tab 1 Content + #tab2.tab-pane + Tab 2 Content diff --git a/spec/javascripts/fixtures/right_sidebar.html.haml b/spec/javascripts/fixtures/right_sidebar.html.haml deleted file mode 100644 index d259b58f235..00000000000 --- a/spec/javascripts/fixtures/right_sidebar.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -%div - %div.page-gutter.page-with-sidebar - - %aside.right-sidebar - %div.block.issuable-sidebar-header - %a.gutter-toggle.pull-right.js-sidebar-toggle - %i.fa.fa-angle-double-left - %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: "1", issuable_type: "issue", url: "/todos" }} - %span.js-issuable-todo-text - Add todo - %i.fa.fa-spin.fa-spinner.js-issuable-todo-loading.hidden - - %form.issuable-context-form - %div.block.labels - %div.sidebar-collapsed-icon - %i.fa.fa-tags - %span 1 diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 51f2ae8bcbd..2db182d702b 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -6,17 +6,21 @@ (function() { window.gon || (window.gon = {}); - - window.disableButtonIfEmptyField = function() { - return null; - }; + window.gl = window.gl || {}; + gl.utils = gl.utils || {}; describe('Notes', function() { - describe('task lists', function() { - fixture.preload('issue_note.html'); + var commentsTemplate = 'issues/issue_with_comment.raw'; + fixture.preload(commentsTemplate); + beforeEach(function () { + fixture.load(commentsTemplate); + gl.utils.disableButtonIfEmptyField = _.noop; + window.project_uploads_path = 'http://test.host/uploads'; + }); + + describe('task lists', function() { beforeEach(function() { - fixture.load('issue_note.html'); $('form').on('submit', function(e) { e.preventDefault(); }); @@ -41,12 +45,9 @@ }); describe('comments', function() { - var commentsTemplate = 'comments.html'; var textarea = '.js-note-text'; - fixture.preload(commentsTemplate); beforeEach(function() { - fixture.load(commentsTemplate); this.notes = new Notes(); this.autoSizeSpy = spyOnEvent($(textarea), 'autosize:update'); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 83ebbd63f3a..0a9bc546144 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -34,9 +34,10 @@ }; describe('RightSidebar', function() { - fixture.preload('right_sidebar.html'); + var fixtureName = 'issues/open-issue.html.raw'; + fixture.preload(fixtureName); beforeEach(function() { - fixture.load('right_sidebar.html'); + fixture.load(fixtureName); this.sidebar = new Sidebar; $aside = $('.right-sidebar'); $page = $('.page-with-sidebar'); @@ -44,15 +45,12 @@ $toggle = $aside.find('.js-sidebar-toggle'); return $labelsIcon = $aside.find('.sidebar-collapsed-icon'); }); - it('should expand the sidebar when arrow is clicked', function() { + it('should expand/collapse the sidebar when arrow is clicked', function() { + assertSidebarState('expanded'); $toggle.click(); - return assertSidebarState('expanded'); - }); - it('should collapse the sidebar when arrow is clicked', function() { + assertSidebarState('collapsed'); $toggle.click(); assertSidebarState('expanded'); - $toggle.click(); - return assertSidebarState('collapsed'); }); it('should float over the page and when sidebar icons clicked', function() { $labelsIcon.click(); diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 7d36d79b687..e37816b0a8c 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -4,9 +4,11 @@ (function() { describe('ShortcutsIssuable', function() { - fixture.preload('issuable.html'); + var fixtureName = 'issues/open-issue.html.raw'; + fixture.preload(fixtureName); beforeEach(function() { - fixture.load('issuable.html'); + fixture.load(fixtureName); + document.querySelector('.js-new-note-form').classList.add('js-main-target-form'); return this.shortcut = new ShortcutsIssuable(); }); return describe('#replyWithSelectedText', function() { diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb index 356dd01a03a..70b31f3a880 100644 --- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb +++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb @@ -22,7 +22,7 @@ describe Banzai::Filter::TableOfContentsFilter, lib: true do html = header(i, "Header #{i}") doc = filter(html) - expect(doc.css("h#{i} a").first.attr('id')).to eq "header-#{i}" + expect(doc.css("h#{i} a").first.attr('id')).to eq "user-content-header-#{i}" end end @@ -32,7 +32,12 @@ describe Banzai::Filter::TableOfContentsFilter, lib: true do expect(doc.css('h1 a').first.attr('class')).to eq 'anchor' end - it 'links to the id' do + it 'has a namespaced id' do + doc = filter(header(1, 'Header')) + expect(doc.css('h1 a').first.attr('id')).to eq 'user-content-header' + end + + it 'links to the non-namespaced id' do doc = filter(header(1, 'Header')) expect(doc.css('h1 a').first.attr('href')).to eq '#header' end @@ -40,29 +45,29 @@ describe Banzai::Filter::TableOfContentsFilter, lib: true do describe 'generated IDs' do it 'translates spaces to dashes' do doc = filter(header(1, 'This header has spaces in it')) - expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-has-spaces-in-it' + expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-has-spaces-in-it' end it 'squeezes multiple spaces and dashes' do doc = filter(header(1, 'This---header is poorly-formatted')) - expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-poorly-formatted' + expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-is-poorly-formatted' end it 'removes punctuation' do doc = filter(header(1, "This, header! is, filled. with @ punctuation?")) - expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-filled-with-punctuation' + expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-is-filled-with-punctuation' end it 'appends a unique number to duplicates' do doc = filter(header(1, 'One') + header(2, 'One')) - expect(doc.css('h1 a').first.attr('id')).to eq 'one' - expect(doc.css('h2 a').first.attr('id')).to eq 'one-1' + expect(doc.css('h1 a').first.attr('href')).to eq '#one' + expect(doc.css('h2 a').first.attr('href')).to eq '#one-1' end it 'supports Unicode' do doc = filter(header(1, '한글')) - expect(doc.css('h1 a').first.attr('id')).to eq '한글' + expect(doc.css('h1 a').first.attr('id')).to eq 'user-content-한글' expect(doc.css('h1 a').first.attr('href')).to eq '#한글' end end diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb index 9aeaa6b3ee8..6062e7af4f5 100644 --- a/spec/lib/gitlab/cycle_analytics/events_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb @@ -321,6 +321,6 @@ describe Gitlab::CycleAnalytics::Events do context.update(milestone: milestone) mr = create_merge_request_closing_issue(context) - ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.sha) + ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash) end end diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb index e5300dbba1e..462caa5b5fe 100644 --- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb @@ -49,14 +49,20 @@ describe Gitlab::GithubImport::BranchFormatter, lib: true do end describe '#valid?' do - it 'returns true when raw repo is present' do + it 'returns true when raw sha and ref are present' do branch = described_class.new(project, double(raw)) expect(branch.valid?).to eq true end - it 'returns false when raw repo is blank' do - branch = described_class.new(project, double(raw.merge(repo: nil))) + it 'returns false when raw sha is blank' do + branch = described_class.new(project, double(raw.merge(sha: nil))) + + expect(branch.valid?).to eq false + end + + it 'returns false when raw ref is blank' do + branch = described_class.new(project, double(raw.merge(ref: nil))) expect(branch.valid?).to eq false end diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb new file mode 100644 index 00000000000..52428547a9f --- /dev/null +++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb @@ -0,0 +1,194 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20161124141322_migrate_process_commit_worker_jobs.rb') + +describe MigrateProcessCommitWorkerJobs do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:commit) { project.commit.raw.raw_commit } + + describe 'Project' do + describe 'find_including_path' do + it 'returns Project instances' do + expect(described_class::Project.find_including_path(project.id)). + to be_an_instance_of(described_class::Project) + end + + it 'selects the full path for every Project' do + migration_project = described_class::Project. + find_including_path(project.id) + + expect(migration_project[:path_with_namespace]). + to eq(project.path_with_namespace) + end + end + + describe '#repository_storage_path' do + it 'returns the storage path for the repository' do + migration_project = described_class::Project. + find_including_path(project.id) + + expect(File.directory?(migration_project.repository_storage_path)). + to eq(true) + end + end + + describe '#repository_path' do + it 'returns the path to the repository' do + migration_project = described_class::Project. + find_including_path(project.id) + + expect(File.directory?(migration_project.repository_path)).to eq(true) + end + end + + describe '#repository' do + it 'returns a Rugged::Repository' do + migration_project = described_class::Project. + find_including_path(project.id) + + expect(migration_project.repository). + to be_an_instance_of(Rugged::Repository) + end + end + end + + describe '#up', :redis do + let(:migration) { described_class.new } + + def job_count + Sidekiq.redis { |r| r.llen('queue:process_commit') } + end + + before do + Sidekiq.redis do |redis| + job = JSON.dump(args: [project.id, user.id, commit.oid]) + redis.lpush('queue:process_commit', job) + end + end + + it 'skips jobs using a project that no longer exists' do + allow(described_class::Project).to receive(:find_including_path). + with(project.id). + and_return(nil) + + migration.up + + expect(job_count).to eq(0) + end + + it 'skips jobs using commits that no longer exist' do + allow_any_instance_of(Rugged::Repository).to receive(:lookup). + with(commit.oid). + and_raise(Rugged::OdbError) + + migration.up + + expect(job_count).to eq(0) + end + + it 'inserts migrated jobs back into the queue' do + migration.up + + expect(job_count).to eq(1) + end + + context 'a migrated job' do + let(:job) do + migration.up + + JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') }) + end + + let(:commit_hash) do + job['args'][2] + end + + it 'includes the project ID' do + expect(job['args'][0]).to eq(project.id) + end + + it 'includes the user ID' do + expect(job['args'][1]).to eq(user.id) + end + + it 'includes the commit ID' do + expect(commit_hash['id']).to eq(commit.oid) + end + + it 'includes the commit message' do + expect(commit_hash['message']).to eq(commit.message) + end + + it 'includes the parent IDs' do + expect(commit_hash['parent_ids']).to eq(commit.parent_ids) + end + + it 'includes the author date' do + expect(commit_hash['authored_date']).to eq(commit.author[:time].to_s) + end + + it 'includes the author name' do + expect(commit_hash['author_name']).to eq(commit.author[:name]) + end + + it 'includes the author Email' do + expect(commit_hash['author_email']).to eq(commit.author[:email]) + end + + it 'includes the commit date' do + expect(commit_hash['committed_date']).to eq(commit.committer[:time].to_s) + end + + it 'includes the committer name' do + expect(commit_hash['committer_name']).to eq(commit.committer[:name]) + end + + it 'includes the committer Email' do + expect(commit_hash['committer_email']).to eq(commit.committer[:email]) + end + end + end + + describe '#down', :redis do + let(:migration) { described_class.new } + + def job_count + Sidekiq.redis { |r| r.llen('queue:process_commit') } + end + + before do + Sidekiq.redis do |redis| + job = JSON.dump(args: [project.id, user.id, commit.oid]) + redis.lpush('queue:process_commit', job) + + migration.up + end + end + + it 'pushes migrated jobs back into the queue' do + migration.down + + expect(job_count).to eq(1) + end + + context 'a migrated job' do + let(:job) do + migration.down + + JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') }) + end + + it 'includes the project ID' do + expect(job['args'][0]).to eq(project.id) + end + + it 'includes the user ID' do + expect(job['args'][1]).to eq(user.id) + end + + it 'includes the commit SHA' do + expect(job['args'][2]).to eq(commit.oid) + end + end + end +end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index ef07f2275b1..d4970e38f7c 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -730,8 +730,8 @@ describe Ci::Build, models: true do pipeline2 = create(:ci_pipeline, project: project) @build2 = create(:ci_build, pipeline: pipeline2) - commits = [double(id: pipeline.sha), double(id: pipeline2.sha)] - allow(@merge_request).to receive(:commits).and_return(commits) + allow(@merge_request).to receive(:commits_sha). + and_return([pipeline.sha, pipeline2.sha]) allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index e3bb3482d67..7194c20d3bf 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -302,4 +302,21 @@ eos expect(commit.uri_type('this/path/doesnt/exist')).to be_nil end end + + describe '.from_hash' do + let(:new_commit) { described_class.from_hash(commit.to_hash, project) } + + it 'returns a Commit' do + expect(new_commit).to be_an_instance_of(described_class) + end + + it 'wraps a Gitlab::Git::Commit' do + expect(new_commit.raw).to be_an_instance_of(Gitlab::Git::Commit) + end + + it 'stores the correct commit fields' do + expect(new_commit.id).to eq(commit.id) + expect(new_commit.message).to eq(commit.message) + end + end end diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index e5007424041..eb876d105da 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -77,24 +77,13 @@ describe MergeRequestDiff, models: true do end describe '#commits_sha' do - shared_examples 'returning all commits SHA' do - it 'returns all commits SHA' do - commits_sha = subject.commits_sha + it 'returns all commits SHA using serialized commits' do + subject.st_commits = [ + { id: 'sha1' }, + { id: 'sha2' } + ] - expect(commits_sha).to eq(subject.commits.map(&:sha)) - end - end - - context 'when commits were loaded' do - before do - subject.commits - end - - it_behaves_like 'returning all commits SHA' - end - - context 'when commits were not loaded' do - it_behaves_like 'returning all commits SHA' + expect(subject.commits_sha).to eq(['sha1', 'sha2']) end end @@ -113,4 +102,15 @@ describe MergeRequestDiff, models: true do expect(diffs.size).to eq(3) end end + + describe '#commits_count' do + it 'returns number of commits using serialized commits' do + subject.st_commits = [ + { id: 'sha1' }, + { id: 'sha2' } + ] + + expect(subject.commits_count).to eq 2 + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 26034cb1c7b..ec22ef93465 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -557,16 +557,13 @@ describe MergeRequest, models: true do end describe '#commits_sha' do - let(:commit0) { double('commit0', sha: 'sha1') } - let(:commit1) { double('commit1', sha: 'sha2') } - let(:commit2) { double('commit2', sha: 'sha3') } - before do - allow(subject.merge_request_diff).to receive(:commits).and_return([commit0, commit1, commit2]) + allow(subject.merge_request_diff).to receive(:commits_sha). + and_return(['sha1']) end - it 'returns sha of commits' do - expect(subject.commits_sha).to contain_exactly('sha1', 'sha2', 'sha3') + it 'delegates to merge request diff' do + expect(subject.commits_sha).to eq ['sha1'] end end @@ -1440,4 +1437,26 @@ describe MergeRequest, models: true do end end end + + describe '#has_commits?' do + before do + allow(subject.merge_request_diff).to receive(:commits_count). + and_return(2) + end + + it 'returns true when merge request diff has commits' do + expect(subject.has_commits?).to be_truthy + end + end + + describe '#has_no_commits?' do + before do + allow(subject.merge_request_diff).to receive(:commits_count). + and_return(0) + end + + it 'returns true when merge request diff has 0 commits' do + expect(subject.has_no_commits?).to be_truthy + end + end end diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb index 01bb9e955e0..36517ad0f8c 100644 --- a/spec/requests/api/api_helpers_spec.rb +++ b/spec/requests/api/api_helpers_spec.rb @@ -47,7 +47,7 @@ describe API::Helpers, api: true do end def error!(message, status) - raise Exception + raise Exception.new("#{status} - #{message}") end describe ".current_user" do @@ -290,4 +290,56 @@ describe API::Helpers, api: true do handle_api_exception(exception) end end + + describe '.authenticate_non_get!' do + %w[HEAD GET].each do |method_name| + context "method is #{method_name}" do + before do + expect_any_instance_of(self.class).to receive(:route).and_return(double(route_method: method_name)) + end + + it 'does not raise an error' do + expect_any_instance_of(self.class).not_to receive(:authenticate!) + + expect { authenticate_non_get! }.not_to raise_error + end + end + end + + %w[POST PUT PATCH DELETE].each do |method_name| + context "method is #{method_name}" do + before do + expect_any_instance_of(self.class).to receive(:route).and_return(double(route_method: method_name)) + end + + it 'calls authenticate!' do + expect_any_instance_of(self.class).to receive(:authenticate!) + + authenticate_non_get! + end + end + end + end + + describe '.authenticate!' do + context 'current_user is nil' do + before do + expect_any_instance_of(self.class).to receive(:current_user).and_return(nil) + end + + it 'returns a 401 response' do + expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}' + end + end + + context 'current_user is present' do + before do + expect_any_instance_of(self.class).to receive(:current_user).and_return(true) + end + + it 'does not raise an error' do + expect { authenticate! }.not_to raise_error + end + end + end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 482e81b29a6..5b3427e66e8 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -200,32 +200,43 @@ describe API::API, api: true do end describe 'GET /projects/visible' do - let(:public_project) { create(:project, :public) } + shared_examples_for 'visible projects response' do + it 'returns the visible projects' do + get api('/projects/visible', current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id)) + end + end + let!(:public_project) { create(:project, :public) } before do - public_project project project2 project3 project4 end - it 'returns the projects viewable by the user' do - get api('/projects/visible', user) - - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.map { |project| project['id'] }). - to contain_exactly(public_project.id, project.id, project2.id, project3.id) + context 'when unauthenticated' do + it_behaves_like 'visible projects response' do + let(:current_user) { nil } + let(:projects) { [public_project] } + end end - it 'shows only public projects when the user only has access to those' do - get api('/projects/visible', user2) + context 'when authenticated' do + it_behaves_like 'visible projects response' do + let(:current_user) { user } + let(:projects) { [public_project, project, project2, project3] } + end + end - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.map { |project| project['id'] }). - to contain_exactly(public_project.id) + context 'when authenticated as a different user' do + it_behaves_like 'visible projects response' do + let(:current_user) { user2 } + let(:projects) { [public_project] } + end end end @@ -528,135 +539,150 @@ describe API::API, api: true do end describe 'GET /projects/:id' do - before { project } - before { project_member } - - it 'returns a project by id' do - group = create(:group) - link = create(:project_group_link, project: project, group: group) + context 'when unauthenticated' do + it 'returns the public projects' do + public_project = create(:project, :public) - get api("/projects/#{project.id}", user) + get api("/projects/#{public_project.id}") - expect(response).to have_http_status(200) - expect(json_response['id']).to eq(project.id) - expect(json_response['description']).to eq(project.description) - expect(json_response['default_branch']).to eq(project.default_branch) - expect(json_response['tag_list']).to be_an Array - expect(json_response['public']).to be_falsey - expect(json_response['archived']).to be_falsey - expect(json_response['visibility_level']).to be_present - expect(json_response['ssh_url_to_repo']).to be_present - expect(json_response['http_url_to_repo']).to be_present - expect(json_response['web_url']).to be_present - expect(json_response['owner']).to be_a Hash - expect(json_response['owner']).to be_a Hash - expect(json_response['name']).to eq(project.name) - expect(json_response['path']).to be_present - expect(json_response['issues_enabled']).to be_present - expect(json_response['merge_requests_enabled']).to be_present - expect(json_response['wiki_enabled']).to be_present - expect(json_response['builds_enabled']).to be_present - expect(json_response['snippets_enabled']).to be_present - expect(json_response['container_registry_enabled']).to be_present - expect(json_response['created_at']).to be_present - expect(json_response['last_activity_at']).to be_present - expect(json_response['shared_runners_enabled']).to be_present - expect(json_response['creator_id']).to be_present - expect(json_response['namespace']).to be_present - expect(json_response['avatar_url']).to be_nil - expect(json_response['star_count']).to be_present - expect(json_response['forks_count']).to be_present - expect(json_response['public_builds']).to be_present - expect(json_response['shared_with_groups']).to be_an Array - expect(json_response['shared_with_groups'].length).to eq(1) - expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) - expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) - expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) - expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds) - expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved) - end - - it 'returns a project by path name' do - get api("/projects/#{project.id}", user) - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(project.name) + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(public_project.id) + expect(json_response['description']).to eq(public_project.description) + expect(json_response.keys).not_to include('permissions') + end end - it 'returns a 404 error if not found' do - get api('/projects/42', user) - expect(response).to have_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') - end + context 'when authenticated' do + before do + project + project_member + end - it 'returns a 404 error if user is not a member' do - other_user = create(:user) - get api("/projects/#{project.id}", other_user) - expect(response).to have_http_status(404) - end + it 'returns a project by id' do + group = create(:group) + link = create(:project_group_link, project: project, group: group) - it 'handles users with dots' do - dot_user = create(:user, username: 'dot.user') - project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace) + get api("/projects/#{project.id}", user) - get api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user) - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(project.name) - end + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(project.id) + expect(json_response['description']).to eq(project.description) + expect(json_response['default_branch']).to eq(project.default_branch) + expect(json_response['tag_list']).to be_an Array + expect(json_response['public']).to be_falsey + expect(json_response['archived']).to be_falsey + expect(json_response['visibility_level']).to be_present + expect(json_response['ssh_url_to_repo']).to be_present + expect(json_response['http_url_to_repo']).to be_present + expect(json_response['web_url']).to be_present + expect(json_response['owner']).to be_a Hash + expect(json_response['owner']).to be_a Hash + expect(json_response['name']).to eq(project.name) + expect(json_response['path']).to be_present + expect(json_response['issues_enabled']).to be_present + expect(json_response['merge_requests_enabled']).to be_present + expect(json_response['wiki_enabled']).to be_present + expect(json_response['builds_enabled']).to be_present + expect(json_response['snippets_enabled']).to be_present + expect(json_response['container_registry_enabled']).to be_present + expect(json_response['created_at']).to be_present + expect(json_response['last_activity_at']).to be_present + expect(json_response['shared_runners_enabled']).to be_present + expect(json_response['creator_id']).to be_present + expect(json_response['namespace']).to be_present + expect(json_response['avatar_url']).to be_nil + expect(json_response['star_count']).to be_present + expect(json_response['forks_count']).to be_present + expect(json_response['public_builds']).to be_present + expect(json_response['shared_with_groups']).to be_an Array + expect(json_response['shared_with_groups'].length).to eq(1) + expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) + expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) + expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) + expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds) + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved) + end + + it 'returns a project by path name' do + get api("/projects/#{project.id}", user) + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(project.name) + end - describe 'permissions' do - context 'all projects' do - before { project.team << [user, :master] } + it 'returns a 404 error if not found' do + get api('/projects/42', user) + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end - it 'contains permission information' do - get api("/projects", user) + it 'returns a 404 error if user is not a member' do + other_user = create(:user) + get api("/projects/#{project.id}", other_user) + expect(response).to have_http_status(404) + end - expect(response).to have_http_status(200) - expect(json_response.first['permissions']['project_access']['access_level']). - to eq(Gitlab::Access::MASTER) - expect(json_response.first['permissions']['group_access']).to be_nil - end + it 'handles users with dots' do + dot_user = create(:user, username: 'dot.user') + project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace) + + get api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user) + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(project.name) end - context 'personal project' do - it 'sets project access and returns 200' do - project.team << [user, :master] - get api("/projects/#{project.id}", user) + describe 'permissions' do + context 'all projects' do + before { project.team << [user, :master] } - expect(response).to have_http_status(200) - expect(json_response['permissions']['project_access']['access_level']). + it 'contains permission information' do + get api("/projects", user) + + expect(response).to have_http_status(200) + expect(json_response.first['permissions']['project_access']['access_level']). to eq(Gitlab::Access::MASTER) - expect(json_response['permissions']['group_access']).to be_nil + expect(json_response.first['permissions']['group_access']).to be_nil + end end - end - context 'group project' do - let(:project2) { create(:project, group: create(:group)) } + context 'personal project' do + it 'sets project access and returns 200' do + project.team << [user, :master] + get api("/projects/#{project.id}", user) - before { project2.group.add_owner(user) } + expect(response).to have_http_status(200) + expect(json_response['permissions']['project_access']['access_level']). + to eq(Gitlab::Access::MASTER) + expect(json_response['permissions']['group_access']).to be_nil + end + end - it 'sets the owner and return 200' do - get api("/projects/#{project2.id}", user) + context 'group project' do + let(:project2) { create(:project, group: create(:group)) } - expect(response).to have_http_status(200) - expect(json_response['permissions']['project_access']).to be_nil - expect(json_response['permissions']['group_access']['access_level']). + before { project2.group.add_owner(user) } + + it 'sets the owner and return 200' do + get api("/projects/#{project2.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['permissions']['project_access']).to be_nil + expect(json_response['permissions']['group_access']['access_level']). to eq(Gitlab::Access::OWNER) + end end end end end describe 'GET /projects/:id/events' do - before { project_member2 } - - context 'valid request' do - before do + shared_examples_for 'project events response' do + it 'returns the project events' do + member = create(:user) + create(:project_member, :developer, user: member, project: project) note = create(:note_on_issue, note: 'What an awesome day!', project: project) EventCreateService.new.leave_note(note, note.author) - end - it 'returns all events' do - get api("/projects/#{project.id}/events", user) + get api("/projects/#{project.id}/events", current_user) expect(response).to have_http_status(200) @@ -669,24 +695,90 @@ describe API::API, api: true do expect(last_event['action_name']).to eq('joined') expect(last_event['project_id'].to_i).to eq(project.id) - expect(last_event['author_username']).to eq(user3.username) - expect(last_event['author']['name']).to eq(user3.name) + expect(last_event['author_username']).to eq(member.username) + expect(last_event['author']['name']).to eq(member.name) end end - it 'returns a 404 error if not found' do - get api('/projects/42/events', user) + context 'when unauthenticated' do + it_behaves_like 'project events response' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end + end - expect(response).to have_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') + context 'when authenticated' do + context 'valid request' do + it_behaves_like 'project events response' do + let(:current_user) { user } + end + end + + it 'returns a 404 error if not found' do + get api('/projects/42/events', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + + it 'returns a 404 error if user is not a member' do + other_user = create(:user) + + get api("/projects/#{project.id}/events", other_user) + + expect(response).to have_http_status(404) + end end + end - it 'returns a 404 error if user is not a member' do - other_user = create(:user) + describe 'GET /projects/:id/users' do + shared_examples_for 'project users response' do + it 'returns the project users' do + member = create(:user) + create(:project_member, :developer, user: member, project: project) - get api("/projects/#{project.id}/events", other_user) + get api("/projects/#{project.id}/users", current_user) - expect(response).to have_http_status(404) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + + first_user = json_response.first + + expect(first_user['username']).to eq(member.username) + expect(first_user['name']).to eq(member.name) + expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url]) + end + end + + context 'when unauthenticated' do + it_behaves_like 'project users response' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end + end + + context 'when authenticated' do + context 'valid request' do + it_behaves_like 'project users response' do + let(:current_user) { user } + end + end + + it 'returns a 404 error if not found' do + get api('/projects/42/users', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + + it 'returns a 404 error if user is not a member' do + other_user = create(:user) + + get api("/projects/#{project.id}/users", other_user) + + expect(response).to have_http_status(404) + end end end @@ -950,35 +1042,37 @@ describe API::API, api: true do let!(:public) { create(:empty_project, :public, name: "public #{query}") } let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') } + shared_examples_for 'project search response' do |args = {}| + it 'returns project search responses' do + get api("/projects/search/#{query}", current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(args[:results]) + json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*query.*/) } + end + end + context 'when unauthenticated' do - it 'returns authentication error' do - get api("/projects/search/#{query}") - expect(response).to have_http_status(401) + it_behaves_like 'project search response', results: 1 do + let(:current_user) { nil } end end context 'when authenticated' do - it 'returns an array of projects' do - get api("/projects/search/#{query}", user) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(6) - json_response.each {|project| expect(project['name']).to match(/.*query.*/)} + it_behaves_like 'project search response', results: 6 do + let(:current_user) { user } end end context 'when authenticated as a different user' do - it 'returns matching public projects' do - get api("/projects/search/#{query}", user2) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(2) - json_response.each {|project| expect(project['name']).to match(/(internal|public) query/)} + it_behaves_like 'project search response', results: 2, match_regex: /(internal|public) query/ do + let(:current_user) { user2 } end end end - describe 'PUT /projects/:id̈́' do + describe 'PUT /projects/:id' do before { project } before { user } before { user3 } diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb index 5c90fd9bad9..f5e0fdcda2d 100644 --- a/spec/requests/projects/cycle_analytics_events_spec.rb +++ b/spec/requests/projects/cycle_analytics_events_spec.rb @@ -135,6 +135,6 @@ describe 'cycle analytics events' do merge_merge_requests_closing_issue(issue) - ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.sha) + ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash) end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 9d7702f5c96..e7624e70725 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -263,7 +263,7 @@ describe GitPushService, services: true do author_email: commit_author.email ) - allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit). + allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit). and_return(commit) allow(project.repository).to receive(:commits_between).and_return([commit]) @@ -321,7 +321,7 @@ describe GitPushService, services: true do committed_date: commit_time ) - allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit). + allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit). and_return(commit) allow(project.repository).to receive(:commits_between).and_return([commit]) @@ -360,7 +360,7 @@ describe GitPushService, services: true do allow(project.repository).to receive(:commits_between). and_return([closing_commit]) - allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit). + allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit). and_return(closing_commit) project.team << [commit_author, :master] diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 7dcd03496bb..90771825f5c 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -7,15 +7,21 @@ describe Projects::DestroyService, services: true do let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") } let!(:async) { false } # execute or async_execute + shared_examples 'deleting the project' do + it 'deletes the project' do + expect(Project.all).not_to include(project) + expect(Dir.exist?(path)).to be_falsey + expect(Dir.exist?(remove_path)).to be_falsey + end + end + context 'Sidekiq inline' do before do # Run sidekiq immediatly to check that renamed repository will be removed Sidekiq::Testing.inline! { destroy_project(project, user, {}) } end - it { expect(Project.all).not_to include(project) } - it { expect(Dir.exist?(path)).to be_falsey } - it { expect(Dir.exist?(remove_path)).to be_falsey } + it_behaves_like 'deleting the project' end context 'Sidekiq fake' do @@ -38,11 +44,21 @@ describe Projects::DestroyService, services: true do Sidekiq::Testing.inline! { destroy_project(project, user, {}) } end - it 'deletes the project' do - expect(Project.all).not_to include(project) - expect(Dir.exist?(path)).to be_falsey - expect(Dir.exist?(remove_path)).to be_falsey + it_behaves_like 'deleting the project' + end + + context 'delete with pipeline' do # which has optimistic locking + let!(:pipeline) { create(:ci_pipeline, project: project) } + + before do + expect(project).to receive(:destroy!).and_call_original + + perform_enqueued_jobs do + destroy_project(project, user, {}) + end end + + it_behaves_like 'deleting the project' end context 'container registry' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ef33c473d38..6ee3307512d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -55,8 +55,12 @@ RSpec.configure do |config| config.around(:each, :redis) do |example| Gitlab::Redis.with(&:flushall) + Sidekiq.redis(&:flushall) + example.run + Gitlab::Redis.with(&:flushall) + Sidekiq.redis(&:flushall) end end diff --git a/spec/support/matchers/have_issuable_counts.rb b/spec/support/matchers/have_issuable_counts.rb index 02605d6b70e..92cf3de5448 100644 --- a/spec/support/matchers/have_issuable_counts.rb +++ b/spec/support/matchers/have_issuable_counts.rb @@ -1,9 +1,9 @@ RSpec::Matchers.define :have_issuable_counts do |opts| - match do |actual| - expected_counts = opts.map do |state, count| - "#{state.to_s.humanize} #{count}" - end + expected_counts = opts.map do |state, count| + "#{state.to_s.humanize} #{count}" + end + match do |actual| actual.within '.issues-state-filters' do expected_counts.each do |expected_count| expect(actual).to have_content(expected_count) diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index 8c98b1f988c..97b8b342eb2 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -38,9 +38,9 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('h1 a#gitlab-markdown') - expect(actual).to have_selector('h2 a#markdown') - expect(actual).to have_selector('h3 a#autolinkfilter') + expect(actual).to have_selector('h1 a#user-content-gitlab-markdown') + expect(actual).to have_selector('h2 a#user-content-markdown') + expect(actual).to have_selector('h3 a#user-content-autolinkfilter') end end diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb index 3e4fee42240..75c7fc1efd2 100644 --- a/spec/workers/process_commit_worker_spec.rb +++ b/spec/workers/process_commit_worker_spec.rb @@ -11,31 +11,25 @@ describe ProcessCommitWorker do it 'does not process the commit when the project does not exist' do expect(worker).not_to receive(:close_issues) - worker.perform(-1, user.id, commit.id) + worker.perform(-1, user.id, commit.to_hash) end it 'does not process the commit when the user does not exist' do expect(worker).not_to receive(:close_issues) - worker.perform(project.id, -1, commit.id) - end - - it 'does not process the commit when the commit no longer exists' do - expect(worker).not_to receive(:close_issues) - - worker.perform(project.id, user.id, 'this-should-does-not-exist') + worker.perform(project.id, -1, commit.to_hash) end it 'processes the commit message' do expect(worker).to receive(:process_commit_message).and_call_original - worker.perform(project.id, user.id, commit.id) + worker.perform(project.id, user.id, commit.to_hash) end it 'updates the issue metrics' do expect(worker).to receive(:update_issue_metrics).and_call_original - worker.perform(project.id, user.id, commit.id) + worker.perform(project.id, user.id, commit.to_hash) end end @@ -106,4 +100,19 @@ describe ProcessCommitWorker do expect(metric.first_mentioned_in_commit_at).to eq(commit.committed_date) end end + + describe '#build_commit' do + it 'returns a Commit' do + commit = worker.build_commit(project, id: '123') + + expect(commit).to be_an_instance_of(Commit) + end + + it 'parses date strings into Time instances' do + commit = worker. + build_commit(project, id: '123', authored_date: Time.now.to_s) + + expect(commit.authored_date).to be_an_instance_of(Time) + end + end end |