diff options
117 files changed, 1060 insertions, 614 deletions
diff --git a/.eslintrc b/.eslintrc index 73cd7ecf66d..c72a5e0335b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -11,6 +11,7 @@ "gon": false, "localStorage": false }, + "parser": "babel-eslint", "plugins": [ "filenames", "import", diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e52b656599c..5723b836c76 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -474,9 +474,10 @@ codeclimate: services: - docker:dind script: + - docker pull stedolan/jq - docker pull codeclimate/codeclimate - - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json - - sed -i.bak 's/\({"body":"\)[^"]*\("}\)/\1\2/g' codeclimate.json + - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json + - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json artifacts: paths: [codeclimate.json] diff --git a/CHANGELOG.md b/CHANGELOG.md index f372cbf91e8..7591559da22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 9.3.3 (2017-06-30) + +- Fix head pipeline stored in merge request for external pipelines. !12478 +- Bring back branches badge to main project page. !12548 +- Fix diff of requirements.txt file by not matching newlines as part of package names. +- Perform housekeeping only when an import of a fresh project is completed. +- Fixed issue boards closed list not showing all closed issues. +- Fixed multi-line markdown tooltip buttons in issue edit form. + ## 9.3.2 (2017-06-27) - API: Fix optional arugments for POST :id/variables. !12474 diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 54d1a4f2a4a..a803cc227fe 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.13.0 +0.14.0 diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index c34d80f0601..18cd04b176a 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -2,7 +2,6 @@ /* global Flash */ import Cookies from 'js-cookie'; -import * as Emoji from './emoji'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; @@ -24,27 +23,9 @@ const categoryLabelMap = { flags: 'Flags', }; -function renderCategory(name, emojiList, opts = {}) { - return ` - <h5 class="emoji-menu-title"> - ${name} - </h5> - <ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}"> - ${emojiList.map(emojiName => ` - <li class="emoji-menu-list-item"> - <button class="emoji-menu-btn text-center js-emoji-btn" type="button"> - ${Emoji.glEmojiTag(emojiName, { - sprite: true, - })} - </button> - </li> - `).join('\n')} - </ul> - `; -} - -export default class AwardsHandler { - constructor() { +class AwardsHandler { + constructor(emoji) { + this.emoji = emoji; this.eventListeners = []; // If the user shows intent let's pre-build the menu this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => { @@ -78,10 +59,10 @@ export default class AwardsHandler { const $target = $(e.currentTarget); const $glEmojiElement = $target.find('gl-emoji'); const $spriteIconElement = $target.find('.icon'); - const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name'); + const emojiName = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name'); $target.closest('.js-awards-block').addClass('current'); - this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji); + this.addAward(this.getVotesBlock(), this.getAwardUrl(), emojiName); }); } @@ -139,16 +120,16 @@ export default class AwardsHandler { this.isCreatingEmojiMenu = true; // Render the first category - const categoryMap = Emoji.getEmojiCategoryMap(); + const categoryMap = this.emoji.getEmojiCategoryMap(); const categoryNameKey = Object.keys(categoryMap)[0]; const emojisInCategory = categoryMap[categoryNameKey]; - const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); + const firstCategory = this.renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); // Render the frequently used const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); let frequentlyUsedCatgegory = ''; if (frequentlyUsedEmojis.length > 0) { - frequentlyUsedCatgegory = renderCategory('Frequently used', frequentlyUsedEmojis, { + frequentlyUsedCatgegory = this.renderCategory('Frequently used', frequentlyUsedEmojis, { menuListClass: 'frequent-emojis', }); } @@ -179,7 +160,7 @@ export default class AwardsHandler { } this.isAddingRemainingEmojiMenuCategories = true; - const categoryMap = Emoji.getEmojiCategoryMap(); + const categoryMap = this.emoji.getEmojiCategoryMap(); // Avoid the jank and render the remaining categories separately // This will take more time, but makes UI more responsive @@ -191,7 +172,7 @@ export default class AwardsHandler { promiseChain.then(() => new Promise((resolve) => { const emojisInCategory = categoryMap[categoryNameKey]; - const categoryMarkup = renderCategory( + const categoryMarkup = this.renderCategory( categoryLabelMap[categoryNameKey], emojisInCategory, ); @@ -216,6 +197,25 @@ export default class AwardsHandler { }); } + renderCategory(name, emojiList, opts = {}) { + return ` + <h5 class="emoji-menu-title"> + ${name} + </h5> + <ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}"> + ${emojiList.map(emojiName => ` + <li class="emoji-menu-list-item"> + <button class="emoji-menu-btn text-center js-emoji-btn" type="button"> + ${this.emoji.glEmojiTag(emojiName, { + sprite: true, + })} + </button> + </li> + `).join('\n')} + </ul> + `; + } + positionMenu($menu, $addBtn) { const position = $addBtn.data('position'); // The menu could potentially be off-screen or in a hidden overflow element @@ -234,7 +234,7 @@ export default class AwardsHandler { } addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { - const normalizedEmoji = Emoji.normalizeEmojiName(emoji); + const normalizedEmoji = this.emoji.normalizeEmojiName(emoji); const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => { this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); @@ -249,7 +249,7 @@ export default class AwardsHandler { this.checkMutuality(votesBlock, emoji); } this.addEmojiToFrequentlyUsedList(emoji); - const normalizedEmoji = Emoji.normalizeEmojiName(emoji); + const normalizedEmoji = this.emoji.normalizeEmojiName(emoji); const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); if ($emojiButton.length > 0) { if (this.isActive($emojiButton)) { @@ -374,7 +374,7 @@ export default class AwardsHandler { createAwardButtonForVotesBlock(votesBlock, emojiName) { const buttonHtml = ` <button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom"> - ${Emoji.glEmojiTag(emojiName)} + ${this.emoji.glEmojiTag(emojiName)} <span class="award-control-text js-counter">1</span> </button> `; @@ -440,7 +440,7 @@ export default class AwardsHandler { } addEmojiToFrequentlyUsedList(emoji) { - if (Emoji.isEmojiNameValid(emoji)) { + if (this.emoji.isEmojiNameValid(emoji)) { this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji)); Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 }); } @@ -450,7 +450,7 @@ export default class AwardsHandler { return this.frequentlyUsedEmojis || (() => { const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(',')); this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter( - inputName => Emoji.isEmojiNameValid(inputName), + inputName => this.emoji.isEmojiNameValid(inputName), ); return this.frequentlyUsedEmojis; @@ -493,7 +493,7 @@ export default class AwardsHandler { } findMatchingEmojiElements(query) { - const emojiMatches = Emoji.filterEmojiNamesByAlias(query); + const emojiMatches = this.emoji.filterEmojiNamesByAlias(query); const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]'); const $matchingElements = $emojiElements .filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0); @@ -507,3 +507,12 @@ export default class AwardsHandler { $('.emoji-menu').remove(); } } + +let awardsHandlerPromise = null; +export default function loadAwardsHandler(reload = false) { + if (!awardsHandlerPromise || reload) { + awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji') + .then(Emoji => new AwardsHandler(Emoji)); + } + return awardsHandlerPromise; +} diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js index 8156e491a42..7e98e04303a 100644 --- a/app/assets/javascripts/behaviors/gl_emoji.js +++ b/app/assets/javascripts/behaviors/gl_emoji.js @@ -1,5 +1,4 @@ import installCustomElements from 'document-register-element'; -import { emojiImageTag, emojiFallbackImageSrc } from '../emoji'; import isEmojiUnicodeSupported from '../emoji/support'; installCustomElements(window); @@ -32,11 +31,19 @@ export default function installGlEmojiElement() { // IE 11 doesn't like adding multiple at once :( this.classList.add('emoji-icon'); this.classList.add(fallbackSpriteClass); - } else if (hasImageFallback) { - this.innerHTML = emojiImageTag(name, fallbackSrc); } else { - const src = emojiFallbackImageSrc(name); - this.innerHTML = emojiImageTag(name, src); + import(/* webpackChunkName: 'emoji' */ '../emoji') + .then(({ emojiImageTag, emojiFallbackImageSrc }) => { + if (hasImageFallback) { + this.innerHTML = emojiImageTag(name, fallbackSrc); + } else { + const src = emojiFallbackImageSrc(name); + this.innerHTML = emojiImageTag(name, src); + } + }) + .catch(() => { + // do nothing + }); } } }; diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 9974e135022..60103155ce0 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -85,9 +85,8 @@ window.Build = (function () { if (!this.hasBeenScrolled) { this.scrollToBottom(); } - }); - - this.verifyTopPosition(); + }) + .then(() => this.verifyTopPosition()); } Build.prototype.canScroll = function () { @@ -176,7 +175,7 @@ window.Build = (function () { } if ($flashError.length) { - topPostion += $flashError.outerHeight(); + topPostion += $flashError.outerHeight() + prependTopDefault; } this.$buildTrace.css({ @@ -234,7 +233,8 @@ window.Build = (function () { if (!this.hasBeenScrolled) { this.scrollToBottom(); } - }); + }) + .then(() => this.verifyTopPosition()); }, 4000); } else { this.$buildRefreshAnimation.remove(); diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 725ec7b9c70..1be9df19c81 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -1,6 +1,7 @@ /* eslint-disable class-methods-use-this */ import './lib/utils/url_utility'; +import FilesCommentButton from './files_comment_button'; const UNFOLD_COUNT = 20; let isBound = false; @@ -8,8 +9,10 @@ let isBound = false; class Diff { constructor() { const $diffFile = $('.files .diff-file'); + $diffFile.singleFileDiff(); - $diffFile.filesCommentButton(); + + FilesCommentButton.init($diffFile); $diffFile.each((index, file) => new gl.ImageFile(file)); diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js index 517bdb6be09..c37249c060a 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -139,9 +139,9 @@ const DiffNoteAvatars = Vue.extend({ const notesCount = this.notesCount; $(this.$el).closest('.js-avatar-container') - .toggleClass('js-no-comment-btn', notesCount > 0) + .toggleClass('no-comment-btn', notesCount > 0) .nextUntil('.js-avatar-container') - .toggleClass('js-no-comment-btn', notesCount > 0); + .toggleClass('no-comment-btn', notesCount > 0); }, toggleDiscussionsToggleState() { const $notesHolders = $(this.$el).closest('.code').find('.notes_holder'); diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 534e651b030..d02e4cd5876 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -1,150 +1,73 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */ -/* global FilesCommentButton */ /* global notes */ -let $commentButtonTemplate; - -window.FilesCommentButton = (function() { - var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; - - COMMENT_BUTTON_CLASS = '.add-diff-note'; - - LINE_HOLDER_CLASS = '.line_holder'; - - LINE_NUMBER_CLASS = 'diff-line-num'; - - LINE_CONTENT_CLASS = 'line_content'; - - UNFOLDABLE_LINE_CLASS = 'js-unfold'; - - EMPTY_CELL_CLASS = 'empty-cell'; - - OLD_LINE_CLASS = 'old_line'; - - LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content"; - - TEXT_FILE_SELECTOR = '.text-file'; - - function FilesCommentButton(filesContainerElement) { - this.render = this.render.bind(this); - this.hideButton = this.hideButton.bind(this); - this.isParallelView = notes.isParallelView(); - filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render) - .on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton); - } - - FilesCommentButton.prototype.render = function(e) { - var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button; - $currentTarget = $(e.currentTarget); - - if ($currentTarget.hasClass('js-no-comment-btn')) return; - - lineContentElement = this.getLineContent($currentTarget); - buttonParentElement = this.getButtonParent($currentTarget); - - if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return; - - $button = $(COMMENT_BUTTON_CLASS, buttonParentElement); - buttonParentElement.addClass('is-over') - .nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over'); - - if ($button.length) { - return; +/* Developer beware! Do not add logic to showButton or hideButton + * that will force a reflow. Doing so will create a signficant performance + * bottleneck for pages with large diffs. For a comprehensive list of what + * causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a + */ + +const LINE_NUMBER_CLASS = 'diff-line-num'; +const UNFOLDABLE_LINE_CLASS = 'js-unfold'; +const NO_COMMENT_CLASS = 'no-comment-btn'; +const EMPTY_CELL_CLASS = 'empty-cell'; +const OLD_LINE_CLASS = 'old_line'; +const LINE_COLUMN_CLASSES = `.${LINE_NUMBER_CLASS}, .line_content`; +const DIFF_CONTAINER_SELECTOR = '.files'; +const DIFF_EXPANDED_CLASS = 'diff-expanded'; + +export default { + init($diffFile) { + /* Caching is used only when the following members are *true*. This is because there are likely to be + * differently configured versions of diffs in the same session. However if these values are true, they + * will be true in all cases */ + + if (!this.userCanCreateNote) { + // data-can-create-note is an empty string when true, otherwise undefined + this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === ''; } - textFileElement = this.getTextFileElement($currentTarget); - buttonParentElement.append(this.buildButton({ - discussionID: lineContentElement.attr('data-discussion-id'), - lineType: lineContentElement.attr('data-line-type'), - - noteableType: textFileElement.attr('data-noteable-type'), - noteableID: textFileElement.attr('data-noteable-id'), - commitID: textFileElement.attr('data-commit-id'), - noteType: lineContentElement.attr('data-note-type'), - - // LegacyDiffNote - lineCode: lineContentElement.attr('data-line-code'), - - // DiffNote - position: lineContentElement.attr('data-position') - })); - }; - - FilesCommentButton.prototype.hideButton = function(e) { - var $currentTarget = $(e.currentTarget); - var buttonParentElement = this.getButtonParent($currentTarget); - - buttonParentElement.removeClass('is-over') - .nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over'); - }; - - FilesCommentButton.prototype.buildButton = function(buttonAttributes) { - return $commentButtonTemplate.clone().attr({ - 'data-discussion-id': buttonAttributes.discussionID, - 'data-line-type': buttonAttributes.lineType, - - 'data-noteable-type': buttonAttributes.noteableType, - 'data-noteable-id': buttonAttributes.noteableID, - 'data-commit-id': buttonAttributes.commitID, - 'data-note-type': buttonAttributes.noteType, - - // LegacyDiffNote - 'data-line-code': buttonAttributes.lineCode, - - // DiffNote - 'data-position': buttonAttributes.position - }); - }; - - FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) { - return hoveredElement.closest(TEXT_FILE_SELECTOR); - }; - - FilesCommentButton.prototype.getLineContent = function(hoveredElement) { - if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) { - return hoveredElement; - } - if (!this.isParallelView) { - return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS); - } else { - return $(hoveredElement).next("." + LINE_CONTENT_CLASS); + if (typeof notes !== 'undefined' && !this.isParallelView) { + this.isParallelView = notes.isParallelView && notes.isParallelView(); } - }; - FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { - if (!this.isParallelView) { - if (hoveredElement.hasClass(OLD_LINE_CLASS)) { - return hoveredElement; - } - return hoveredElement.parent().find("." + OLD_LINE_CLASS); - } else { - if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) { - return hoveredElement; - } - return $(hoveredElement).prev("." + LINE_NUMBER_CLASS); + if (this.userCanCreateNote) { + $diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e)) + .on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e)); } - }; + }, - FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { - return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS); - }; + showButton(isParallelView, e) { + const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView); - FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { - return lineContentElement.attr('data-note-type') && lineContentElement.attr('data-note-type') !== ''; - }; + if (!this.validateButtonParent(buttonParentElement)) return; - return FilesCommentButton; -})(); + buttonParentElement.classList.add('is-over'); + buttonParentElement.nextElementSibling.classList.add('is-over'); + }, -$.fn.filesCommentButton = function() { - $commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>'); + hideButton(isParallelView, e) { + const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView); - if (!(this && (this.parent().data('can-create-note') != null))) { - return; - } - return this.each(function() { - if (!$.data(this, 'filesCommentButton')) { - return $.data(this, 'filesCommentButton', new FilesCommentButton($(this))); + buttonParentElement.classList.remove('is-over'); + buttonParentElement.nextElementSibling.classList.remove('is-over'); + }, + + getButtonParent(hoveredElement, isParallelView) { + if (isParallelView) { + if (!hoveredElement.classList.contains(LINE_NUMBER_CLASS)) { + return hoveredElement.previousElementSibling; + } + } else if (!hoveredElement.classList.contains(OLD_LINE_CLASS)) { + return hoveredElement.parentNode.querySelector(`.${OLD_LINE_CLASS}`); } - }); + return hoveredElement; + }, + + validateButtonParent(buttonParentElement) { + return !buttonParentElement.classList.contains(EMPTY_CELL_CLASS) && + !buttonParentElement.classList.contains(UNFOLDABLE_LINE_CLASS) && + !buttonParentElement.classList.contains(NO_COMMENT_CLASS) && + !buttonParentElement.parentNode.classList.contains(DIFF_EXPANDED_CLASS); + }, }; diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 10a64f9032b..2c56b718212 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -1,4 +1,3 @@ -import { validEmojiNames, glEmojiTag } from './emoji'; import glRegexp from './lib/utils/regexp'; import AjaxCache from './lib/utils/ajax_cache'; @@ -373,7 +372,12 @@ class GfmAutoComplete { if (this.cachedData[at]) { this.loadData($input, at, this.cachedData[at]); } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { - this.loadData($input, at, validEmojiNames); + import(/* webpackChunkName: 'emoji' */ './emoji') + .then(({ validEmojiNames, glEmojiTag }) => { + this.loadData($input, at, validEmojiNames); + GfmAutoComplete.glEmojiTag = glEmojiTag; + }) + .catch(() => { this.isLoadingData[at] = false; }); } else { AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true) .then((data) => { @@ -428,12 +432,14 @@ GfmAutoComplete.atTypeMap = { }; // Emoji +GfmAutoComplete.glEmojiTag = null; GfmAutoComplete.Emoji = { templateFunction(name) { - return `<li> - ${name} ${glEmojiTag(name)} - </li> - `; + // glEmojiTag helper is loaded on-demand in fetchData() + if (GfmAutoComplete.glEmojiTag) { + return `<li>${name} ${GfmAutoComplete.glEmojiTag(name)}</li>`; + } + return `<li>${name}</li>`; }, }; // Team Members diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js index 462d792b8d5..37c6765d942 100644 --- a/app/assets/javascripts/group_name.js +++ b/app/assets/javascripts/group_name.js @@ -1,13 +1,13 @@ - +import Cookies from 'js-cookie'; import _ from 'underscore'; export default class GroupName { constructor() { - this.titleContainer = document.querySelector('.title-container'); - this.title = document.querySelector('.title'); + this.titleContainer = document.querySelector('.js-title-container'); + this.title = this.titleContainer.querySelector('.title'); this.titleWidth = this.title.offsetWidth; - this.groupTitle = document.querySelector('.group-title'); - this.groups = document.querySelectorAll('.group-path'); + this.groupTitle = this.titleContainer.querySelector('.group-title'); + this.groups = this.titleContainer.querySelectorAll('.group-path'); this.toggle = null; this.isHidden = false; this.init(); @@ -33,11 +33,20 @@ export default class GroupName { createToggle() { this.toggle = document.createElement('button'); + this.toggle.setAttribute('type', 'button'); this.toggle.className = 'text-expander group-name-toggle'; this.toggle.setAttribute('aria-label', 'Toggle full path'); - this.toggle.innerHTML = '...'; + if (Cookies.get('new_nav') === 'true') { + this.toggle.innerHTML = '<i class="fa fa-ellipsis-h" aria-hidden="true"></i>'; + } else { + this.toggle.innerHTML = '...'; + } this.toggle.addEventListener('click', this.toggleGroups.bind(this)); - this.titleContainer.insertBefore(this.toggle, this.title); + if (Cookies.get('new_nav') === 'true') { + this.title.insertBefore(this.toggle, this.groupTitle); + } else { + this.titleContainer.insertBefore(this.toggle, this.title); + } this.toggleGroups(); } diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index a8856120c5e..4f376599ba9 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -5,6 +5,7 @@ /* global SubscriptionSelect */ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; +import SidebarHeightManager from './sidebar_height_manager'; const HIDDEN_CLASS = 'hidden'; const DISABLED_CONTENT_CLASS = 'disabled-content'; @@ -56,18 +57,6 @@ export default class IssuableBulkUpdateSidebar { return navbarHeight + layoutNavHeight + subNavScroll; } - initSidebar() { - if (!this.navHeight) { - this.navHeight = this.getNavHeight(); - } - - if (!this.sidebarInitialized) { - $(document).off('scroll').on('scroll', _.throttle(this.setSidebarHeight, 10).bind(this)); - $(window).off('resize').on('resize', _.throttle(this.setSidebarHeight, 10).bind(this)); - this.sidebarInitialized = true; - } - } - setupBulkUpdateActions() { IssuableBulkUpdateActions.setOriginalDropdownData(); } @@ -97,7 +86,7 @@ export default class IssuableBulkUpdateSidebar { this.toggleCheckboxDisplay(enable); if (enable) { - this.initSidebar(); + SidebarHeightManager.init(); } } @@ -143,17 +132,6 @@ export default class IssuableBulkUpdateSidebar { this.$bulkEditSubmitBtn.enable(); } } - // loosely based on method of the same name in right_sidebar.js - setSidebarHeight() { - const currentScrollDepth = window.pageYOffset || 0; - const diff = this.navHeight - currentScrollDepth; - - if (diff > 0) { - this.$sidebar.outerHeight(window.innerHeight - diff); - } else { - this.$sidebar.outerHeight('100%'); - } - } static getCheckedIssueIds() { const $checkedIssues = $('.selected_issue:checked'); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index bfcc50996cc..1d1763c3963 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -112,29 +112,11 @@ window.dateFormat = dateFormat; return timefor; }; - w.gl.utils.cachedTimeagoElements = []; w.gl.utils.renderTimeago = function($els) { - if (!$els && !w.gl.utils.cachedTimeagoElements.length) { - w.gl.utils.cachedTimeagoElements = [].slice.call(document.querySelectorAll('.js-timeago-render')); - } else if ($els) { - w.gl.utils.cachedTimeagoElements = w.gl.utils.cachedTimeagoElements.concat($els.toArray()); - } - - w.gl.utils.cachedTimeagoElements.forEach(gl.utils.updateTimeagoText); - }; - - w.gl.utils.updateTimeagoText = function(el) { - const formattedDate = gl.utils.getTimeago().format(el.getAttribute('datetime'), lang); - - if (el.textContent !== formattedDate) { - el.textContent = formattedDate; - } - }; - - w.gl.utils.initTimeagoTimeout = function() { - gl.utils.renderTimeago(); + const timeagoEls = $els || document.querySelectorAll('.js-timeago-render'); - gl.utils.timeagoTimeout = setTimeout(gl.utils.initTimeagoTimeout, 1000); + // timeago.js sets timeouts internally for each timeago value to be updated in real time + gl.utils.getTimeago().render(timeagoEls, lang); }; w.gl.utils.getDayDifference = function(a, b) { diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index d27b4ec78c6..fe752d95b90 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -70,7 +70,7 @@ import './ajax_loading_spinner'; import './api'; import './aside'; import './autosave'; -import AwardsHandler from './awards_handler'; +import loadAwardsHandler from './awards_handler'; import './breakpoints'; import './broadcast_message'; import './build'; @@ -355,10 +355,10 @@ $(function () { $window.off('resize.app').on('resize.app', function () { return fitSidebarForSize(); }); - gl.awardsHandler = new AwardsHandler(); + loadAwardsHandler(); new Aside(); - gl.utils.initTimeagoTimeout(); + gl.utils.renderTimeago(); $(document).trigger('init.scrolling-tabs'); }); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 3cf3233cc65..7840f05a8ae 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -144,7 +144,9 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; this.resetViewContainer(); this.mountPipelinesView(); } else { - this.expandView(); + if (Breakpoints.get().getBreakpointSize() !== 'xs') { + this.expandView(); + } this.resetViewContainer(); this.destroyPipelinesView(); } diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 34476f3303f..555b8c8a65c 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -18,6 +18,7 @@ import 'vendor/jquery.caret'; // required by jquery.atwho import 'vendor/jquery.atwho'; import AjaxCache from '~/lib/utils/ajax_cache'; import CommentTypeToggle from './comment_type_toggle'; +import loadAwardsHandler from './awards_handler'; import './autosave'; import './dropzone_input'; import './task_list'; @@ -291,8 +292,13 @@ export default class Notes { if ('emoji_award' in noteEntity.commands_changes) { votesBlock = $('.js-awards-block').eq(0); - gl.awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award); - return gl.awardsHandler.scrollToAwards(); + + loadAwardsHandler().then((awardsHandler) => { + awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award); + awardsHandler.scrollToAwards(); + }).catch(() => { + // ignore + }); } } } @@ -337,6 +343,10 @@ export default class Notes { if (!noteEntity.valid) { if (noteEntity.errors.commands_only) { + if (noteEntity.commands_changes && + Object.keys(noteEntity.commands_changes).length > 0) { + $notesList.find('.system-note.being-posted').remove(); + } this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline); this.refresh(); } @@ -829,6 +839,8 @@ export default class Notes { */ setupDiscussionNoteForm(dataHolder, form) { // setup note target + const diffFileData = dataHolder.closest('.text-file'); + var discussionID = dataHolder.data('discussionId'); if (discussionID) { @@ -839,9 +851,10 @@ export default class Notes { form.attr('data-line-code', dataHolder.data('lineCode')); form.find('#line_type').val(dataHolder.data('lineType')); - form.find('#note_noteable_type').val(dataHolder.data('noteableType')); - form.find('#note_noteable_id').val(dataHolder.data('noteableId')); - form.find('#note_commit_id').val(dataHolder.data('commitId')); + form.find('#note_noteable_type').val(diffFileData.data('noteableType')); + form.find('#note_noteable_id').val(diffFileData.data('noteableId')); + form.find('#note_commit_id').val(diffFileData.data('commitId')); + form.find('#note_type').val(dataHolder.data('noteType')); // LegacyDiffNote diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index b5cd01044a3..d8f1fe10b26 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -1,6 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */ import Cookies from 'js-cookie'; +import SidebarHeightManager from './sidebar_height_manager'; (function() { this.Sidebar = (function() { @@ -8,12 +9,6 @@ import Cookies from 'js-cookie'; this.toggleTodo = this.toggleTodo.bind(this); this.sidebar = $('aside'); - this.$sidebarInner = this.sidebar.find('.issuable-sidebar'); - this.$navGitlab = $('.navbar-gitlab'); - this.$layoutNav = $('.layout-nav'); - this.$subScroll = $('.sub-nav-scroll'); - this.$rightSidebar = $('.js-right-sidebar'); - this.removeListeners(); this.addEventListeners(); } @@ -27,16 +22,14 @@ import Cookies from 'js-cookie'; }; Sidebar.prototype.addEventListeners = function() { + SidebarHeightManager.init(); const $document = $(document); - const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 20); - const slowerThrottledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 200); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); - $(window).on('resize', () => throttledSetSidebarHeight()); - $document.on('scroll', () => slowerThrottledSetSidebarHeight()); + $document.on('click', '.js-sidebar-toggle', function(e, triggered) { var $allGutterToggleIcons, $this, $thisIcon; e.preventDefault(); @@ -214,18 +207,6 @@ import Cookies from 'js-cookie'; } }; - Sidebar.prototype.setSidebarHeight = function() { - const $navHeight = this.$navGitlab.outerHeight() + this.$layoutNav.outerHeight() + (this.$subScroll ? this.$subScroll.outerHeight() : 0); - const diff = $navHeight - $(window).scrollTop(); - if (diff > 0) { - this.$rightSidebar.outerHeight($(window).height() - diff); - this.$sidebarInner.height('100%'); - } else { - this.$rightSidebar.outerHeight('100%'); - this.$sidebarInner.height(''); - } - }; - Sidebar.prototype.isOpen = function() { return this.sidebar.is('.right-sidebar-expanded'); }; diff --git a/app/assets/javascripts/sidebar_height_manager.js b/app/assets/javascripts/sidebar_height_manager.js new file mode 100644 index 00000000000..022415f22b2 --- /dev/null +++ b/app/assets/javascripts/sidebar_height_manager.js @@ -0,0 +1,33 @@ +export default { + init() { + if (!this.initialized) { + this.$window = $(window); + this.$rightSidebar = $('.js-right-sidebar'); + this.$navHeight = $('.navbar-gitlab').outerHeight() + + $('.layout-nav').outerHeight() + + $('.sub-nav-scroll').outerHeight(); + + const throttledSetSidebarHeight = _.throttle(() => this.setSidebarHeight(), 20); + const debouncedSetSidebarHeight = _.debounce(() => this.setSidebarHeight(), 200); + + this.$window.on('scroll', throttledSetSidebarHeight); + this.$window.on('resize', debouncedSetSidebarHeight); + this.initialized = true; + } + }, + + setSidebarHeight() { + const currentScrollDepth = window.pageYOffset || 0; + const diff = this.$navHeight - currentScrollDepth; + + if (diff > 0) { + const newSidebarHeight = window.innerHeight - diff; + this.$rightSidebar.outerHeight(newSidebarHeight); + this.sidebarHeightIsCustom = true; + } else if (this.sidebarHeightIsCustom) { + this.$rightSidebar.outerHeight('100%'); + this.sidebarHeightIsCustom = false; + } + }, +}; + diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index c44892dae3d..9316a2af0b7 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -1,5 +1,7 @@ /* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */ +import FilesCommentButton from './files_comment_button'; + (function() { window.SingleFileDiff = (function() { var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; @@ -78,6 +80,8 @@ gl.diffNotesCompileComponents(); } + FilesCommentButton.init($(_this.file)); + if (cb) cb(); }; })(this)); diff --git a/app/assets/javascripts/webpack.js b/app/assets/javascripts/webpack.js new file mode 100644 index 00000000000..9a9cf395fb8 --- /dev/null +++ b/app/assets/javascripts/webpack.js @@ -0,0 +1,9 @@ +/** + * This is the first script loaded by webpack's runtime. It is used to manually configure + * config.output.publicPath to account for relative_url_root or CDN settings which cannot be + * baked-in to our webpack bundles. + */ + +if (gon && gon.webpack_public_path) { + __webpack_public_path__ = gon.webpack_public_path; // eslint-disable-line +} diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index 3ce5b4fd073..bfb7a0c7e25 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -264,3 +264,127 @@ header.navbar-gitlab-new { } } } + +.breadcrumbs { + display: flex; + min-height: 60px; + padding-top: $gl-padding-top; + padding-bottom: $gl-padding-top; + color: $gl-text-color; + border-bottom: 1px solid $border-color; + + .dropdown-toggle-caret { + position: relative; + top: -1px; + padding: 0 5px; + color: rgba($black, .65); + font-size: 10px; + line-height: 1; + background: none; + border: 0; + + &:focus { + outline: 0; + } + } +} + +.breadcrumbs-container { + display: flex; + width: 100%; + position: relative; + + .dropdown-menu-projects { + margin-top: -$gl-padding; + margin-left: $gl-padding; + } +} + +.breadcrumbs-links { + flex: 1; + align-self: center; + color: $black-transparent; + + a { + color: rgba($black, .65); + + &:not(:first-child), + &.group-path { + margin-left: 4px; + } + + &:not(:last-of-type), + &.group-path { + margin-right: 3px; + } + } + + .title { + white-space: nowrap; + + > a { + &:last-of-type { + font-weight: 600; + } + } + } + + .avatar-tile { + margin-right: 5px; + border: 1px solid $border-color; + border-radius: 50%; + vertical-align: sub; + + &.identicon { + float: left; + width: 16px; + height: 16px; + margin-top: 2px; + font-size: 10px; + } + } + + .text-expander { + margin-left: 4px; + margin-right: 4px; + + > i { + position: relative; + top: 1px; + } + } +} + +.breadcrumbs-extra { + flex: 0 0 auto; + margin-left: auto; +} + +.breadcrumbs-sub-title { + margin: 2px 0 0; + font-size: 16px; + font-weight: normal; + + ul { + margin: 0; + } + + li { + display: inline-block; + + &:not(:last-child) { + &::after { + content: "/"; + margin: 0 2px 0 5px; + } + } + + &:last-child a { + font-weight: 600; + } + } + + a { + color: $gl-text-color; + } +} diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index be4cc02b3ea..17f23f7fce3 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -49,6 +49,7 @@ $new-sidebar-width: 220px; position: fixed; z-index: 400; width: $new-sidebar-width; + transition: width $sidebar-transition-duration; top: 50px; bottom: 0; left: 0; @@ -62,6 +63,8 @@ $new-sidebar-width: 220px; } li { + white-space: nowrap; + a { display: block; padding: 12px 14px; @@ -72,6 +75,10 @@ $new-sidebar-width: 220px; color: $gl-text-color; text-decoration: none; } + + @media (max-width: $screen-xs-max) { + width: 0; + } } .sidebar-sub-level-items { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index b58922626fa..55011e8a21b 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -20,8 +20,6 @@ } .diff-content { - overflow: auto; - overflow-y: hidden; background: $white-light; color: $gl-text-color; border-radius: 0 0 3px 3px; @@ -476,6 +474,7 @@ height: 19px; width: 19px; margin-left: -15px; + z-index: 100; &:hover { .diff-comment-avatar, @@ -491,7 +490,7 @@ transform: translateX((($i * $x-pos) - $x-pos)); &:hover { - transform: translateX((($i * $x-pos) - $x-pos)) scale(1.2); + transform: translateX((($i * $x-pos) - $x-pos)); } } } @@ -542,6 +541,7 @@ height: 19px; padding: 0; transition: transform .1s ease-out; + z-index: 100; svg { position: absolute; @@ -555,10 +555,6 @@ fill: $white-light; } - &:hover { - transform: scale(1.2); - } - &:focus { outline: 0; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 53d5cf2f7bc..303425041df 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -628,8 +628,14 @@ ul.notes { * Line note button on the side of diffs */ +.line_holder .is-over:not(.no-comment-btn) { + .add-diff-note { + opacity: 1; + } +} + .add-diff-note { - display: none; + opacity: 0; margin-top: -2px; border-radius: 50%; background: $white-light; @@ -642,13 +648,11 @@ ul.notes { width: 23px; height: 23px; border: 1px solid $blue-500; - transition: transform .1s ease-in-out; &:hover { background: $blue-500; border-color: $blue-600; color: $white-light; - transform: scale(1.15); } &:active { diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 558f8b5e2e5..7bc2117f61e 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -20,6 +20,7 @@ # class IssuableFinder NONE = '0'.freeze + IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze attr_accessor :current_user, :params @@ -62,7 +63,7 @@ class IssuableFinder # grouping and counting within that query. # def count_by_state - count_params = params.merge(state: nil, sort: nil) + count_params = params.merge(state: nil, sort: nil, for_counting: true) labels_count = label_names.any? ? label_names.count : 1 finder = self.class.new(current_user, count_params) counts = Hash.new(0) @@ -86,6 +87,10 @@ class IssuableFinder execute.find_by!(*params) end + def state_counter_cache_key(state) + Digest::SHA1.hexdigest(state_counter_cache_key_components(state).flatten.join('-')) + end + def group return @group if defined?(@group) @@ -418,4 +423,13 @@ class IssuableFinder def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' end + + def state_counter_cache_key_components(state) + opts = params.with_indifferent_access + opts[:state] = state + opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY) + opts.delete_if { |_, value| value.blank? } + + ['issuables_count', klass.to_ability_name, opts.sort] + end end diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 3da5508aefd..85230ff1293 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -16,14 +16,72 @@ # sort: string # class IssuesFinder < IssuableFinder + CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER + def klass Issue end + def with_confidentiality_access_check + return Issue.all if user_can_see_all_confidential_issues? + return Issue.where('issues.confidential IS NOT TRUE') if user_cannot_see_confidential_issues? + + Issue.where(' + issues.confidential IS NOT TRUE + OR (issues.confidential = TRUE + AND (issues.author_id = :user_id + OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id) + OR issues.project_id IN(:project_ids)))', + user_id: current_user.id, + project_ids: current_user.authorized_projects(CONFIDENTIAL_ACCESS_LEVEL).select(:id)) + end + private def init_collection - IssuesFinder.not_restricted_by_confidentiality(current_user) + with_confidentiality_access_check + end + + def user_can_see_all_confidential_issues? + return @user_can_see_all_confidential_issues if defined?(@user_can_see_all_confidential_issues) + + return @user_can_see_all_confidential_issues = false if current_user.blank? + return @user_can_see_all_confidential_issues = true if current_user.full_private_access? + + @user_can_see_all_confidential_issues = + project? && + project && + project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL + end + + # Anonymous users can't see any confidential issues. + # + # Users without access to see _all_ confidential issues (as in + # `user_can_see_all_confidential_issues?`) are more complicated, because they + # can see confidential issues where: + # 1. They are an assignee. + # 2. They are an author. + # + # That's fine for most cases, but if we're just counting, we need to cache + # effectively. If we cached this accurately, we'd have a cache key for every + # authenticated user without sufficient access to the project. Instead, when + # we are counting, we treat them as if they can't see any confidential issues. + # + # This does mean the counts may be wrong for those users, but avoids an + # explosion in cache keys. + def user_cannot_see_confidential_issues?(for_counting: false) + return false if user_can_see_all_confidential_issues? + + current_user.blank? || for_counting || params[:for_counting] + end + + def state_counter_cache_key_components(state) + extra_components = [ + user_can_see_all_confidential_issues?, + user_cannot_see_confidential_issues?(for_counting: true) + ] + + super + extra_components end def by_assignee(items) @@ -38,21 +96,6 @@ class IssuesFinder < IssuableFinder end end - def self.not_restricted_by_confidentiality(user) - return Issue.where('issues.confidential IS NOT TRUE') if user.blank? - - return Issue.all if user.full_private_access? - - Issue.where(' - issues.confidential IS NOT TRUE - OR (issues.confidential = TRUE - AND (issues.author_id = :user_id - OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id) - OR issues.project_id IN(:project_ids)))', - user_id: user.id, - project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id)) - end - def item_project_ids(items) items&.reorder(nil)&.select(:project_id) end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index eb45241615f..af0b3e9c5bc 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -16,11 +16,12 @@ module GroupsHelper full_title = '' group.ancestors.reverse.each do |parent| - full_title += link_to(simple_sanitize(parent.name), group_path(parent), class: 'group-path hidable') + full_title += group_title_link(parent, hidable: true) + full_title += '<span class="hidable"> / </span>'.html_safe end - full_title += link_to(simple_sanitize(group.name), group_path(group), class: 'group-path') + full_title += group_title_link(group) full_title += ' · '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name content_tag :span, class: 'group-title' do @@ -56,4 +57,20 @@ module GroupsHelper def group_issues(group) IssuesFinder.new(current_user, group_id: group.id).execute end + + private + + def group_title_link(group, hidable: false) + link_to(group_path(group), class: "group-path #{'hidable' if hidable}") do + output = + if show_new_nav? + image_tag(group_icon(group), class: "avatar-tile", width: 16, height: 16) + else + "" + end + + output << simple_sanitize(group.name) + output.html_safe + end + end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 3259a9c1933..05177e58c5a 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -165,11 +165,7 @@ module IssuablesHelper } state_title = titles[state] || state.to_s.humanize - - count = - Rails.cache.fetch(issuables_state_counter_cache_key(issuable_type, state), expires_in: 2.minutes) do - issuables_count_for_state(issuable_type, state) - end + count = issuables_count_for_state(issuable_type, state) html = content_tag(:span, state_title) html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge') @@ -237,6 +233,18 @@ module IssuablesHelper } end + def issuables_count_for_state(issuable_type, state, finder: nil) + finder ||= public_send("#{issuable_type}_finder") + cache_key = finder.state_counter_cache_key(state) + + @counts ||= {} + @counts[cache_key] ||= Rails.cache.fetch(cache_key, expires_in: 2.minutes) do + finder.count_by_state + end + + @counts[cache_key][state] + end + private def sidebar_gutter_collapsed? @@ -255,24 +263,6 @@ module IssuablesHelper end end - def issuables_count_for_state(issuable_type, state) - @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].freeze - private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY - - def issuables_state_counter_cache_key(issuable_type, state) - 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 - def issuable_templates(issuable) @issuable_templates ||= case issuable diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 64ad7b280cb..ecc6cd6c6c5 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -47,6 +47,18 @@ module NotesHelper data end + def add_diff_note_button(line_code, position, line_type) + return if @diff_notes_disabled + + button_tag '', + class: 'add-diff-note js-add-diff-note-button', + type: 'submit', name: 'button', + data: diff_view_line_data(line_code, position, line_type), + title: 'Add a comment to this line' do + icon('comment-o') + end + end + def link_to_reply_discussion(discussion, line_type = nil) return unless current_user diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c04b1419a19..53d95c2de94 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -58,7 +58,17 @@ module ProjectsHelper link_to(simple_sanitize(owner.name), user_path(owner)) end - project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } + project_link = link_to project_path(project), { class: "project-item-select-holder" } do + output = + if show_new_nav? + project_icon(project, alt: project.name, class: 'avatar-tile', width: 16, height: 16) + else + "" + end + + output << simple_sanitize(project.name) + output.html_safe + end if current_user project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb index 6bacda9fe75..0386df22374 100644 --- a/app/helpers/webpack_helper.rb +++ b/app/helpers/webpack_helper.rb @@ -11,20 +11,29 @@ module WebpackHelper paths = Webpack::Rails::Manifest.asset_paths(source) if extension - paths = paths.select { |p| p.ends_with? ".#{extension}" } + paths.select! { |p| p.ends_with? ".#{extension}" } end - # include full webpack-dev-server url for rspec tests running locally + force_host = webpack_public_host + if force_host + paths.map! { |p| "#{force_host}#{p}" } + end + + paths + end + + def webpack_public_host if Rails.env.test? && Rails.configuration.webpack.dev_server.enabled host = Rails.configuration.webpack.dev_server.host port = Rails.configuration.webpack.dev_server.port protocol = Rails.configuration.webpack.dev_server.https ? 'https' : 'http' - - paths.map! do |p| - "#{protocol}://#{host}:#{port}#{p}" - end + "#{protocol}://#{host}:#{port}" + else + ActionController::Base.asset_host.try(:chomp, '/') end + end - paths + def webpack_public_path + "#{webpack_public_host}/#{Rails.application.config.webpack.public_path}/" end end diff --git a/app/models/concerns/mentionable/reference_regexes.rb b/app/models/concerns/mentionable/reference_regexes.rb index 1848230ec7e..2d86a70c395 100644 --- a/app/models/concerns/mentionable/reference_regexes.rb +++ b/app/models/concerns/mentionable/reference_regexes.rb @@ -14,7 +14,7 @@ module Mentionable end EXTERNAL_PATTERN = begin - issue_pattern = ExternalIssue.reference_pattern + issue_pattern = IssueTrackerService.reference_pattern link_patterns = URI.regexp(%w(http https)) reference_pattern(link_patterns, issue_pattern) end diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index e63f89a9f85..0bf18e529f0 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -38,11 +38,6 @@ class ExternalIssue @project.id end - # Pattern used to extract `JIRA-123` issue references from text - def self.reference_pattern - @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} - end - def to_reference(_from_project = nil, full: nil) id end diff --git a/app/models/project.rb b/app/models/project.rb index 5374aca7701..1513402132b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -737,8 +737,8 @@ class Project < ActiveRecord::Base end end - def issue_reference_pattern - issues_tracker.reference_pattern + def external_issue_reference_pattern + external_issue_tracker.class.reference_pattern end def default_issues_tracker? diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index ff138b9066d..fcc7c4bec06 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -5,7 +5,10 @@ class IssueTrackerService < Service # Pattern used to extract links from comments # Override this method on services that uses different patterns - def reference_pattern + # This pattern does not support cross-project references + # The other code assumes that this pattern is a superset of all + # overriden patterns. See ReferenceRegexes::EXTERNAL_PATTERN + def self.reference_pattern @reference_pattern ||= %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)} end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 2450fb43212..00328892b4a 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -18,7 +18,7 @@ class JiraService < IssueTrackerService end # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1 - def reference_pattern + def self.reference_pattern @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} end diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb index d222d1e63aa..eab65d09299 100644 --- a/app/services/git_hooks_service.rb +++ b/app/services/git_hooks_service.rb @@ -3,8 +3,8 @@ class GitHooksService attr_accessor :oldrev, :newrev, :ref - def execute(user, repo_path, oldrev, newrev, ref) - @repo_path = repo_path + def execute(user, project, oldrev, newrev, ref) + @project = project @user = Gitlab::GlId.gl_id(user) @oldrev = oldrev @newrev = newrev @@ -26,7 +26,7 @@ class GitHooksService private def run_hook(name) - hook = Gitlab::Git::Hook.new(name, @repo_path) + hook = Gitlab::Git::Hook.new(name, @project) hook.trigger(@user, oldrev, newrev, ref) end end diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index ed6ea638235..43636fde0be 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -120,7 +120,7 @@ class GitOperationService def with_hooks(ref, newrev, oldrev) GitHooksService.new.execute( user, - repository.path_to_repo, + repository.project, oldrev, newrev, ref) do |service| diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml index f893c3e1675..ad35d05c29a 100644 --- a/app/views/dashboard/activity.html.haml +++ b/app/views/dashboard/activity.html.haml @@ -1,3 +1,4 @@ +- @hide_top_links = true - @no_container = true = content_for :meta_tags do diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index f9b45a539a1..1cea8182733 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -1,3 +1,4 @@ +- @hide_top_links = true - page_title "Groups" - header_title "Groups", dashboard_groups_path = render 'dashboard/groups_head' diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index 664ec618b79..ef1467c4d78 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -1,3 +1,4 @@ +- @hide_top_links = true - page_title 'Milestones' - header_title 'Milestones', dashboard_milestones_path diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index 5e63a61e21b..7ac6cf06fb9 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -1,4 +1,6 @@ - @no_container = true +- @hide_top_links = true +- @breadcrumb_title = "Projects" = content_for :meta_tags do = auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity") diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index 85cbe0bf0e6..e86b1ab3116 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -1,3 +1,4 @@ +- @hide_top_links = true - page_title "Snippets" - header_title "Snippets", dashboard_snippets_path diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index cc710f4ec7d..abb6dc2e9f3 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -38,7 +38,7 @@ = Gon::Base.render_data - = webpack_bundle_tag "runtime" + = webpack_bundle_tag "webpack_runtime" = webpack_bundle_tag "common" = webpack_bundle_tag "locale" = webpack_bundle_tag "main" diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 62a76a1b00e..1a9f5401a78 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -14,6 +14,8 @@ = render "layouts/broadcast" = render "layouts/flash" = yield :flash_message + - if show_new_nav? + = render "layouts/nav/breadcrumbs" %div{ class: "#{(container_class unless @no_container)} #{@content_class}" } .content{ id: "content-body" } = yield diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index f056c0af968..8cbc3f6105f 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -17,7 +17,7 @@ = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do = brand_header_logo - .title-container + .title-container.js-title-container %h1.title{ class: ('initializing' if @has_group_title) }= title .navbar-collapse.collapse diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml index c0833c64911..5859f689dd1 100644 --- a/app/views/layouts/header/_new.html.haml +++ b/app/views/layouts/header/_new.html.haml @@ -83,8 +83,6 @@ = icon('ellipsis-v', class: 'js-navbar-toggle-right') = icon('times', class: 'js-navbar-toggle-left', style: 'display: none;') - = yield :header_content - = render 'shared/outdated_browser' - if @project && !@project.empty_repo? diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml new file mode 100644 index 00000000000..5f1641f4300 --- /dev/null +++ b/app/views/layouts/nav/_breadcrumbs.html.haml @@ -0,0 +1,19 @@ +- breadcrumb_title = @breadcrumb_title || controller.controller_name.humanize +- hide_top_links = @hide_top_links || false + +%nav.breadcrumbs{ role: "navigation" } + .breadcrumbs-container{ class: container_class } + .breadcrumbs-links.js-title-container + - unless hide_top_links + .title + = link_to "GitLab", root_path + \/ + = header_title + %h2.breadcrumbs-sub-title + %ul.list-unstyled + - if content_for?(:sub_title_before) + = yield :sub_title_before + %li= link_to breadcrumb_title, request.path + - if content_for?(:breadcrumbs_extra) + .breadcrumbs-extra.hidden-xs= yield :breadcrumbs_extra + = yield :header_content diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 68024d782a6..b095adcfe7e 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -28,7 +28,7 @@ %span Issues - if @project.default_issues_tracker? - %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) + %span.badge.count.issue_counter= number_with_delimiter(issuables_count_for_state(:issues, :opened, finder: IssuesFinder.new(current_user, project_id: @project.id))) - if project_nav_tab? :merge_requests - controllers = [:merge_requests, 'projects/merge_requests/conflicts'] @@ -37,7 +37,7 @@ = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %span Merge Requests - %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) + %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(issuables_count_for_state(:merge_requests, :opened, finder: MergeRequestsFinder.new(current_user, project_id: @project.id))) - if project_nav_tab? :pipelines = nav_link(controller: [:pipelines, :builds, :environments, :artifacts]) do diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index 6684ecfce81..3622720a8b7 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -2,6 +2,10 @@ - @content_class = "issue-boards-content" - page_title "Boards" +- if show_new_nav? + - content_for :sub_title_before do + %li= link_to "Issues", namespace_project_issues_path(@project.namespace, @project) + - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'filtered_search' diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 43708d22a0c..cd0fb21f8a7 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -19,6 +19,7 @@ - if plain = link_text - else + = add_diff_note_button(line_code, diff_file.position(line), type) %a{ href: "##{line_code}", data: { linenumber: link_text } } - discussion = line_discussions.try(:first) - if discussion && discussion.resolvable? && !plain @@ -29,7 +30,7 @@ = link_text - else %a{ href: "##{line_code}", data: { linenumber: link_text } } - %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }< + %td.line_content.noteable_line{ class: type }< - if email %pre= line.text - else diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 8e5f4d2573d..56d63250714 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -1,4 +1,5 @@ / Side-by-side diff view + .text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data } %table - diff_file.parallel_diff_lines.each do |line| @@ -18,11 +19,12 @@ - left_line_code = diff_file.line_code(left) - left_position = diff_file.position(left) %td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } } + = add_diff_note_button(left_line_code, left_position, 'old') %a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } } - discussion_left = discussions_left.try(:first) - if discussion_left && discussion_left.resolvable? %diff-note-avatars{ "discussion-id" => discussion_left.id } - %td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text) + %td.line_content.parallel.noteable_line{ class: left.type }= diff_line_content(left.text) - else %td.old_line.diff-line-num.empty-cell %td.line_content.parallel @@ -38,11 +40,12 @@ - right_line_code = diff_file.line_code(right) - right_position = diff_file.position(right) %td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } } + = add_diff_note_button(right_line_code, right_position, 'new') %a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } } - discussion_right = discussions_right.try(:first) - if discussion_right && discussion_right.resolvable? %diff-note-avatars{ "discussion-id" => discussion_right.id } - %td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text) + %td.line_content.parallel.noteable_line{ class: right.type }= diff_line_content(right.text) - else %td.old_line.diff-line-num.empty-cell %td.line_content.parallel diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml new file mode 100644 index 00000000000..698959ec74f --- /dev/null +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -0,0 +1,11 @@ += link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do + = icon('rss') +- if @can_bulk_update + = button_tag "Edit Issues", class: "btn btn-default append-right-10 js-bulk-update-toggle" += link_to "New issue", new_namespace_project_issue_path(@project.namespace, + @project, + issue: { assignee_id: issues_finder.assignee.try(:id), + milestone_id: issues_finder.milestones.first.try(:id) }), + class: "btn btn-new", + title: "New issue", + id: "new_issue_link" diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 7183794ce72..89ac5ff7128 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -13,23 +13,16 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues") +- if show_new_nav? + - content_for :breadcrumbs_extra do + = render "projects/issues/nav_btns" + - if project_issues(@project).exists? %div{ class: (container_class) } .top-area = render 'shared/issuable/nav', type: :issues - .nav-controls - = link_to params.merge(rss_url_options), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do - = icon('rss') - - if @can_bulk_update - = button_tag "Edit Issues", class: "btn btn-default js-bulk-update-toggle" - = link_to new_namespace_project_issue_path(@project.namespace, - @project, - issue: { assignee_id: issues_finder.assignee.try(:id), - milestone_id: issues_finder.milestones.first.try(:id) }), - class: "btn btn-new", - title: "New issue", - id: "new_issue_link" do - New issue + .nav-controls{ class: ("visible-xs" if show_new_nav?) } + = render "projects/issues/nav_btns" = render 'shared/issuable/search_bar', type: :issues - if @can_bulk_update diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml new file mode 100644 index 00000000000..e92f2712347 --- /dev/null +++ b/app/views/projects/merge_requests/_nav_btns.html.haml @@ -0,0 +1,5 @@ +- if @can_bulk_update + = button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle" +- if merge_project + = link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do + New merge request diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 86996e488a1..6fe44ba3c3d 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,5 +1,7 @@ - @no_container = true - @can_bulk_update = can?(current_user, :admin_merge_request, @project) +- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) +- new_merge_request_path = namespace_project_new_merge_request_path(merge_project.namespace, merge_project) if merge_project - page_title "Merge Requests" - unless @project.default_issues_tracker? @@ -10,6 +12,9 @@ = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'filtered_search' +- if show_new_nav? + - content_for :breadcrumbs_extra do + = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path = render 'projects/last_push' @@ -17,13 +22,8 @@ %div{ class: container_class } .top-area = render 'shared/issuable/nav', type: :merge_requests - .nav-controls - - if @can_bulk_update - = button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle" - - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - - if merge_project - = link_to namespace_project_new_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New merge request" do - New merge request + .nav-controls{ class: ("visible-xs" if show_new_nav?) } + = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path = render 'shared/issuable/search_bar', type: :merge_requests @@ -33,4 +33,4 @@ .merge-requests-holder = render 'merge_requests' - else - = render 'shared/empty_states/merge_requests', button_path: namespace_project_new_merge_request_path(@project.namespace, @project) + = render 'shared/empty_states/merge_requests', button_path: new_merge_request_path diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml index 3a49227961f..49555b6ff4e 100644 --- a/app/views/shared/_issues.html.haml +++ b/app/views/shared/_issues.html.haml @@ -1,6 +1,6 @@ - if @issues.to_a.any? .panel.panel-default.panel-small.panel-without-border - %ul.content-list.issues-list + %ul.content-list.issues-list.issuable-list = render partial: 'projects/issues/issue', collection: @issues = paginate @issues, theme: "gitlab" - else diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml index eecbb32e90e..0517896cfbd 100644 --- a/app/views/shared/_merge_requests.html.haml +++ b/app/views/shared/_merge_requests.html.haml @@ -1,6 +1,6 @@ - if @merge_requests.to_a.any? .panel.panel-default.panel-small.panel-without-border - %ul.content-list.mr-list + %ul.content-list.mr-list.issuable-list = render partial: 'projects/merge_requests/merge_request', collection: @merge_requests = paginate @merge_requests, theme: "gitlab" diff --git a/changelogs/unreleased/adam-external-issue-references-spike.yml b/changelogs/unreleased/adam-external-issue-references-spike.yml new file mode 100644 index 00000000000..aeec6688425 --- /dev/null +++ b/changelogs/unreleased/adam-external-issue-references-spike.yml @@ -0,0 +1,4 @@ +--- +title: Improve support for external issue references +merge_request: 12485 +author: diff --git a/changelogs/unreleased/dm-dependency-linker-newlines.yml b/changelogs/unreleased/dm-dependency-linker-newlines.yml deleted file mode 100644 index 5631095fcb7..00000000000 --- a/changelogs/unreleased/dm-dependency-linker-newlines.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix diff of requirements.txt file by not matching newlines as part of package - names -merge_request: -author: diff --git a/changelogs/unreleased/dm-empty-state-new-merge-request.yml b/changelogs/unreleased/dm-empty-state-new-merge-request.yml new file mode 100644 index 00000000000..5fad7a0f883 --- /dev/null +++ b/changelogs/unreleased/dm-empty-state-new-merge-request.yml @@ -0,0 +1,5 @@ +--- +title: Fix 'New merge request' button for users who don't have push access to canonical + project +merge_request: +author: diff --git a/changelogs/unreleased/enable-webpack-code-splitting.yml b/changelogs/unreleased/enable-webpack-code-splitting.yml new file mode 100644 index 00000000000..d61c3b97d11 --- /dev/null +++ b/changelogs/unreleased/enable-webpack-code-splitting.yml @@ -0,0 +1,5 @@ +--- +title: Enable support for webpack code-splitting by dynamically setting publicPath + at runtime +merge_request: 12032 +author: diff --git a/changelogs/unreleased/fix-34417.yml b/changelogs/unreleased/fix-34417.yml deleted file mode 100644 index 5f012ad0c81..00000000000 --- a/changelogs/unreleased/fix-34417.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Perform housekeeping only when an import of a fresh project is completed -merge_request: -author: diff --git a/changelogs/unreleased/fix-assigned-issuable-lists.yml b/changelogs/unreleased/fix-assigned-issuable-lists.yml new file mode 100644 index 00000000000..fc2cd18ddb6 --- /dev/null +++ b/changelogs/unreleased/fix-assigned-issuable-lists.yml @@ -0,0 +1,5 @@ +--- +title: Add issuable-list class to shared mr/issue lists to fix new responsive layout + design +merge_request: +author: diff --git a/changelogs/unreleased/fix-head-pipeline-for-commit-status.yml b/changelogs/unreleased/fix-head-pipeline-for-commit-status.yml deleted file mode 100644 index f12e7b53790..00000000000 --- a/changelogs/unreleased/fix-head-pipeline-for-commit-status.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix head pipeline stored in merge request for external pipelines -merge_request: 12478 -author: diff --git a/changelogs/unreleased/fix-sidebar-showing-mobile-merge-requests.yml b/changelogs/unreleased/fix-sidebar-showing-mobile-merge-requests.yml new file mode 100644 index 00000000000..856990a6126 --- /dev/null +++ b/changelogs/unreleased/fix-sidebar-showing-mobile-merge-requests.yml @@ -0,0 +1,4 @@ +--- +title: Fixed sidebar not collapsing on merge requests in mobile screens +merge_request: +author: diff --git a/changelogs/unreleased/highest-return-on-diff-investment.yml b/changelogs/unreleased/highest-return-on-diff-investment.yml deleted file mode 100644 index c8be1e0ff8f..00000000000 --- a/changelogs/unreleased/highest-return-on-diff-investment.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Bring back branches badge to main project page -merge_request: 12548 -author: diff --git a/changelogs/unreleased/issue-boards-closed-list-all.yml b/changelogs/unreleased/issue-boards-closed-list-all.yml deleted file mode 100644 index 7643864150d..00000000000 --- a/changelogs/unreleased/issue-boards-closed-list-all.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed issue boards closed list not showing all closed issues -merge_request: -author: diff --git a/changelogs/unreleased/issue-form-multiple-line-markdown.yml b/changelogs/unreleased/issue-form-multiple-line-markdown.yml deleted file mode 100644 index 23128f346bc..00000000000 --- a/changelogs/unreleased/issue-form-multiple-line-markdown.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed multi-line markdown tooltip buttons in issue edit form -merge_request: -author: diff --git a/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml b/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml new file mode 100644 index 00000000000..6bf03d9a382 --- /dev/null +++ b/changelogs/unreleased/speed-up-issue-counting-for-a-project.yml @@ -0,0 +1,5 @@ +--- +title: Cache open issue and merge request counts for project tabs to speed up project + pages +merge_request: 12457 +author: diff --git a/config/webpack.config.js b/config/webpack.config.js index 90ef6a5448b..cbb0a899638 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -71,6 +71,7 @@ var config = { vue_merge_request_widget: './vue_merge_request_widget/index.js', test: './test.js', peek: './peek.js', + webpack_runtime: './webpack.js', }, output: { @@ -190,7 +191,7 @@ var config = { // create cacheable common library bundles new webpack.optimize.CommonsChunkPlugin({ - names: ['main', 'locale', 'common', 'runtime'], + names: ['main', 'locale', 'common', 'webpack_runtime'], }), ], @@ -245,7 +246,6 @@ if (IS_DEV_SERVER) { hot: DEV_SERVER_LIVERELOAD, inline: DEV_SERVER_LIVERELOAD }; - config.output.publicPath = '//' + DEV_SERVER_HOST + ':' + DEV_SERVER_PORT + config.output.publicPath; config.plugins.push( // watch node_modules for changes if we encounter a missing module compile error new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules')) diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index 265c891cf83..2dd9b33273c 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -8,6 +8,9 @@ you to do the following: issue index of the external tracker - clicking **New issue** on the project dashboard creates a new issue on the external tracker +- you can reference these external issues inside GitLab interface + (merge requests, commits, comments) and they will be automatically converted + into links ## Configuration diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png Binary files differindex b636cb294b8..cf7f519f783 100644 --- a/doc/user/project/img/issue_board.png +++ b/doc/user/project/img/issue_board.png diff --git a/doc/user/project/img/issue_board_add_list.png b/doc/user/project/img/issue_board_add_list.png Binary files differindex cdfc466d23f..973d9f7cde4 100644 --- a/doc/user/project/img/issue_board_add_list.png +++ b/doc/user/project/img/issue_board_add_list.png diff --git a/doc/user/project/img/issue_board_welcome_message.png b/doc/user/project/img/issue_board_welcome_message.png Binary files differindex 5318e6ea4a9..127b9b08cc7 100644 --- a/doc/user/project/img/issue_board_welcome_message.png +++ b/doc/user/project/img/issue_board_welcome_message.png diff --git a/doc/user/project/img/issue_boards_add_issues_modal.png b/doc/user/project/img/issue_boards_add_issues_modal.png Binary files differindex 33049dce74f..bedaf724a15 100644 --- a/doc/user/project/img/issue_boards_add_issues_modal.png +++ b/doc/user/project/img/issue_boards_add_issues_modal.png diff --git a/doc/user/project/integrations/bugzilla.md b/doc/user/project/integrations/bugzilla.md index 0b219e84478..6a040516231 100644 --- a/doc/user/project/integrations/bugzilla.md +++ b/doc/user/project/integrations/bugzilla.md @@ -16,3 +16,14 @@ Once you have configured and enabled Bugzilla: - the **Issues** link on the GitLab project pages takes you to the appropriate Bugzilla product page - clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue + +## Referencing issues in Bugzilla + +Issues in Bugzilla can be referenced in two alternative ways: +1. `#<ID>` where `<ID>` is a number (example `#143`) +2. `<PROJECT>-<ID>` where `<PROJECT>` starts with a capital letter which is + then followed by capital letters, numbers or underscores, and `<ID>` is + a number (example `API_32-143`). + +Please note that `<PROJECT>` part is ignored and links always point to the +address specified in `issues_url`. diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md index 89c0312d3c2..8026f1f57bc 100644 --- a/doc/user/project/integrations/redmine.md +++ b/doc/user/project/integrations/redmine.md @@ -21,3 +21,14 @@ Once you have configured and enabled Redmine: As an example, below is a configuration for a project named gitlab-ci.  + +## Referencing issues in Redmine + +Issues in Redmine can be referenced in two alternative ways: +1. `#<ID>` where `<ID>` is a number (example `#143`) +2. `<PROJECT>-<ID>` where `<PROJECT>` starts with a capital letter which is + then followed by capital letters, numbers or underscores, and `<ID>` is + a number (example `API_32-143`). + +Please note that `<PROJECT>` part is ignored and links always point to the +address specified in `issues_url`. diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 36fc315599e..2c59ec5bb06 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -232,7 +232,7 @@ module SharedDiffNote end def click_parallel_diff_line(code, line_type) - find(".line_content.parallel.#{line_type}[data-line-code='#{code}']").trigger 'mouseover' + find(".line_holder.parallel .diff-line-num[id='#{code}']").trigger 'mouseover' find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click' end end diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 8bc2dd18bda..7a262dd025c 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -216,12 +216,7 @@ module Banzai @references_per_project ||= begin refs = Hash.new { |hash, key| hash[key] = Set.new } - regex = - if uses_reference_pattern? - Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern) - else - object_class.link_reference_pattern - end + regex = Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern) nodes.each do |node| node.to_html.scan(regex) do @@ -323,14 +318,6 @@ module Banzai value end end - - # There might be special cases like filters - # that should ignore reference pattern - # eg: IssueReferenceFilter when using a external issues tracker - # In those cases this method should be overridden on the filter subclass - def uses_reference_pattern? - true - end end end end diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb index dce4de3ceaf..53a229256a5 100644 --- a/lib/banzai/filter/external_issue_reference_filter.rb +++ b/lib/banzai/filter/external_issue_reference_filter.rb @@ -3,6 +3,8 @@ module Banzai # HTML filter that replaces external issue tracker references with links. # References are ignored if the project doesn't use an external issue # tracker. + # + # This filter does not support cross-project references. class ExternalIssueReferenceFilter < ReferenceFilter self.reference_type = :external_issue @@ -87,7 +89,7 @@ module Banzai end def issue_reference_pattern - external_issues_cached(:issue_reference_pattern) + external_issues_cached(:external_issue_reference_pattern) end private diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb index 044d18ff824..ba1a5ac84b3 100644 --- a/lib/banzai/filter/issue_reference_filter.rb +++ b/lib/banzai/filter/issue_reference_filter.rb @@ -15,10 +15,6 @@ module Banzai Issue end - def uses_reference_pattern? - context[:project].default_issues_tracker? - end - def find_object(project, iid) issues_per_project[project][iid] end @@ -38,13 +34,7 @@ module Banzai projects_per_reference.each do |path, project| issue_ids = references_per_project[path] - - issues = - if project.default_issues_tracker? - project.issues.where(iid: issue_ids.to_a) - else - issue_ids.map { |id| ExternalIssue.new(id, project) } - end + issues = project.issues.where(iid: issue_ids.to_a) issues.each do |issue| hash[project][issue.iid.to_i] = issue @@ -55,26 +45,6 @@ module Banzai end end - def object_link_title(object) - if object.is_a?(ExternalIssue) - "Issue in #{object.project.external_issue_tracker.title}" - else - super - end - end - - def data_attributes_for(text, project, object, link: false) - if object.is_a?(ExternalIssue) - data_attribute( - project: project.id, - external_issue: object.id, - reference_type: ExternalIssueReferenceFilter.reference_type - ) - else - super - end - end - def projects_relation_for_paths(paths) super(paths).includes(:gitlab_issue_tracker_service) end diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb index 9fd4bd68d43..a65bbe23958 100644 --- a/lib/banzai/reference_parser/issue_parser.rb +++ b/lib/banzai/reference_parser/issue_parser.rb @@ -4,9 +4,6 @@ module Banzai self.reference_type = :issue def nodes_visible_to_user(user, nodes) - # It is not possible to check access rights for external issue trackers - return nodes if project && project.external_issue_tracker - issues = issues_for_nodes(nodes) readable_issues = Ability diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb index 897dcff8012..6555c589173 100644 --- a/lib/gitlab/ci/config/entry/image.rb +++ b/lib/gitlab/ci/config/entry/image.rb @@ -15,7 +15,7 @@ module Gitlab validates :config, allowed_keys: ALLOWED_KEYS validates :name, type: String, presence: true - validates :entrypoint, type: String, allow_nil: true + validates :entrypoint, array_of_strings: true, allow_nil: true end def hash? diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb index b52faf48b58..3e2ebcff31a 100644 --- a/lib/gitlab/ci/config/entry/service.rb +++ b/lib/gitlab/ci/config/entry/service.rb @@ -15,8 +15,8 @@ module Gitlab validates :config, allowed_keys: ALLOWED_KEYS validates :name, type: String, presence: true - validates :entrypoint, type: String, allow_nil: true - validates :command, type: String, allow_nil: true + validates :entrypoint, array_of_strings: true, allow_nil: true + validates :command, array_of_strings: true, allow_nil: true validates :alias, type: String, allow_nil: true end diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb index bd90d24a2ec..5042916343b 100644 --- a/lib/gitlab/git/hook.rb +++ b/lib/gitlab/git/hook.rb @@ -4,9 +4,10 @@ module Gitlab GL_PROTOCOL = 'web'.freeze attr_reader :name, :repo_path, :path - def initialize(name, repo_path) + def initialize(name, project) @name = name - @repo_path = repo_path + @project = project + @repo_path = project.repository.path @path = File.join(repo_path.strip, 'hooks', name) end @@ -38,7 +39,8 @@ module Gitlab vars = { 'GL_ID' => gl_id, 'PWD' => repo_path, - 'GL_PROTOCOL' => GL_PROTOCOL + 'GL_PROTOCOL' => GL_PROTOCOL, + 'GL_REPOSITORY' => Gitlab::GlRepository.gl_repository(@project, false) } options = { diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 319633656ff..2d1ae6a5925 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -2,11 +2,14 @@ module Gitlab module GonHelper + include WebpackHelper + def add_gon_variables gon.api_version = 'v4' gon.default_avatar_url = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s gon.max_file_size = current_application_settings.max_attachment_size gon.asset_host = ActionController::Base.asset_host + gon.webpack_public_path = webpack_public_path gon.relative_url_root = Gitlab.config.gitlab.relative_url_root gon.shortcuts_path = help_page_path('shortcuts') gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class diff --git a/package.json b/package.json index 045f07ee2f9..5a997e813f8 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "babel-core": "^6.22.1", + "babel-eslint": "^7.2.1", "babel-loader": "^6.2.10", "babel-plugin-transform-define": "^1.2.0", "babel-preset-latest": "^6.24.0", diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index aef1c17a239..1bb2db11e7f 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -220,7 +220,7 @@ FactoryGirl.define do active: true, properties: { 'project_url' => 'http://redmine/projects/project_name_in_redmine', - 'issues_url' => "http://redmine/#{project.id}/project_name_in_redmine/:id", + 'issues_url' => 'http://redmine/projects/project_name_in_redmine/issues/:id', 'new_issue_url' => 'http://redmine/projects/project_name_in_redmine/issues/new' } ) diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index ea749528c11..d492a15ea17 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -129,7 +129,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do before do large_diff.find('.diff-line-num', match: :prefer_exact).hover - large_diff.find('.add-diff-note').click + large_diff.find('.add-diff-note', match: :prefer_exact).click large_diff.find('.note-textarea').send_keys comment_text large_diff.find_button('Comment').click wait_for_requests diff --git a/spec/features/issuables/user_sees_sidebar_spec.rb b/spec/features/issuables/user_sees_sidebar_spec.rb new file mode 100644 index 00000000000..4d7a7dc1806 --- /dev/null +++ b/spec/features/issuables/user_sees_sidebar_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +describe 'Issue Sidebar on Mobile' do + include MobileHelpers + + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:issue) { create(:issue, project: project) } + let!(:user) { create(:user)} + + before do + sign_in(user) + end + + context 'mobile sidebar on merge requests', js: true do + before do + visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) + end + + it_behaves_like "issue sidebar stays collapsed on mobile" + end + + context 'mobile sidebar on issues', js: true do + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it_behaves_like "issue sidebar stays collapsed on mobile" + end +end diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 163bc4bb32f..09724781a27 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -154,20 +154,6 @@ feature 'Issue Sidebar', feature: true do end end - context 'as a allowed mobile user', js: true do - before do - project.team << [user, :developer] - resize_screen_xs - visit_issue(project, issue) - end - - context 'mobile sidebar' do - it 'collapses the sidebar for small screens' do - expect(page).not_to have_css('aside.right-sidebar.right-sidebar-collapsed') - end - end - end - context 'as a guest' do before do project.team << [user, :guest] diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index a111aa87c52..3f8d2255298 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -98,6 +98,6 @@ feature 'Import/Export - project import integration test', feature: true, js: tr end def project_hook_exists?(project) - Gitlab::Git::Hook.new('post-receive', project.repository.path).exists? + Gitlab::Git::Hook.new('post-receive', project).exists? end end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 8ace1fb5751..4a52f0d5c58 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -295,22 +295,121 @@ describe IssuesFinder do end end - describe '.not_restricted_by_confidentiality' do - let(:authorized_user) { create(:user) } - let(:project) { create(:empty_project, namespace: authorized_user.namespace) } - let!(:public_issue) { create(:issue, project: project) } - let!(:confidential_issue) { create(:issue, project: project, confidential: true) } - - it 'returns non confidential issues for nil user' do - expect(described_class.send(:not_restricted_by_confidentiality, nil)).to include(public_issue) - end + describe '#with_confidentiality_access_check' do + let(:guest) { create(:user) } + set(:authorized_user) { create(:user) } + set(:project) { create(:empty_project, namespace: authorized_user.namespace) } + set(:public_issue) { create(:issue, project: project) } + set(:confidential_issue) { create(:issue, project: project, confidential: true) } + + context 'when no project filter is given' do + let(:params) { {} } + + context 'for an anonymous user' do + subject { described_class.new(nil, params).with_confidentiality_access_check } + + it 'returns only public issues' do + expect(subject).to include(public_issue) + expect(subject).not_to include(confidential_issue) + end + end + + context 'for a user without project membership' do + subject { described_class.new(user, params).with_confidentiality_access_check } + + it 'returns only public issues' do + expect(subject).to include(public_issue) + expect(subject).not_to include(confidential_issue) + end + end + + context 'for a guest user' do + subject { described_class.new(guest, params).with_confidentiality_access_check } + + before do + project.add_guest(guest) + end + + it 'returns only public issues' do + expect(subject).to include(public_issue) + expect(subject).not_to include(confidential_issue) + end + end + + context 'for a project member with access to view confidential issues' do + subject { described_class.new(authorized_user, params).with_confidentiality_access_check } - it 'returns non confidential issues for user not authorized for the issues projects' do - expect(described_class.send(:not_restricted_by_confidentiality, user)).to include(public_issue) + it 'returns all issues' do + expect(subject).to include(public_issue, confidential_issue) + end + end end - it 'returns all issues for user authorized for the issues projects' do - expect(described_class.send(:not_restricted_by_confidentiality, authorized_user)).to include(public_issue, confidential_issue) + context 'when searching within a specific project' do + let(:params) { { project_id: project.id } } + + context 'for an anonymous user' do + subject { described_class.new(nil, params).with_confidentiality_access_check } + + it 'returns only public issues' do + expect(subject).to include(public_issue) + expect(subject).not_to include(confidential_issue) + end + + it 'does not filter by confidentiality' do + expect(Issue).not_to receive(:where).with(a_string_matching('confidential'), anything) + + subject + end + end + + context 'for a user without project membership' do + subject { described_class.new(user, params).with_confidentiality_access_check } + + it 'returns only public issues' do + expect(subject).to include(public_issue) + expect(subject).not_to include(confidential_issue) + end + + it 'filters by confidentiality' do + expect(Issue).to receive(:where).with(a_string_matching('confidential'), anything) + + subject + end + end + + context 'for a guest user' do + subject { described_class.new(guest, params).with_confidentiality_access_check } + + before do + project.add_guest(guest) + end + + it 'returns only public issues' do + expect(subject).to include(public_issue) + expect(subject).not_to include(confidential_issue) + end + + it 'filters by confidentiality' do + expect(Issue).to receive(:where).with(a_string_matching('confidential'), anything) + + subject + end + end + + context 'for a project member with access to view confidential issues' do + subject { described_class.new(authorized_user, params).with_confidentiality_access_check } + + it 'returns all issues' do + expect(subject).to include(public_issue, confidential_issue) + end + + it 'does not filter by confidentiality' do + expect(Issue).not_to receive(:where).with(a_string_matching('confidential'), anything) + + subject + end + end end end end diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 8da22dc78fa..e3f9d9db9eb 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -91,7 +91,7 @@ describe GroupsHelper do let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } it 'outputs the groups in the correct order' do - expect(group_title(very_deep_nested_group)).to match(/>#{group.name}<\/a>.*>#{nested_group.name}<\/a>.*>#{deep_nested_group.name}<\/a>/) + expect(helper.group_title(very_deep_nested_group)).to match(/>#{group.name}<\/a>.*>#{nested_group.name}<\/a>.*>#{deep_nested_group.name}<\/a>/) end end end diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 15cb620199d..d2e918ef014 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -77,54 +77,89 @@ describe IssuablesHelper do }.with_indifferent_access end + let(:issues_finder) { IssuesFinder.new(nil, params) } + let(:merge_requests_finder) { MergeRequestsFinder.new(nil, params) } + + before do + allow(helper).to receive(:issues_finder).and_return(issues_finder) + allow(helper).to receive(:merge_requests_finder).and_return(merge_requests_finder) + end + it 'returns the cached value when called for the same issuable type & with the same params' do - expect(helper).to receive(:params).twice.and_return(params) - expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42) + expect(issues_finder).to receive(:count_by_state).and_return(opened: 42) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('<span>Open</span> <span class="badge">42</span>') - expect(helper).not_to receive(:issuables_count_for_state) + expect(issues_finder).not_to receive(:count_by_state) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('<span>Open</span> <span class="badge">42</span>') end + it 'takes confidential status into account when searching for issues' do + expect(issues_finder).to receive(:count_by_state).and_return(opened: 42) + + expect(helper.issuables_state_counter_text(:issues, :opened)) + .to include('42') + + expect(issues_finder).to receive(:user_cannot_see_confidential_issues?).twice.and_return(false) + expect(issues_finder).to receive(:count_by_state).and_return(opened: 40) + + expect(helper.issuables_state_counter_text(:issues, :opened)) + .to include('40') + + expect(issues_finder).to receive(:user_can_see_all_confidential_issues?).and_return(true) + expect(issues_finder).to receive(:count_by_state).and_return(opened: 45) + + expect(helper.issuables_state_counter_text(:issues, :opened)) + .to include('45') + end + + it 'does not take confidential status into account when searching for merge requests' do + expect(merge_requests_finder).to receive(:count_by_state).and_return(opened: 42) + expect(merge_requests_finder).not_to receive(:user_cannot_see_confidential_issues?) + expect(merge_requests_finder).not_to receive(:user_can_see_all_confidential_issues?) + + expect(helper.issuables_state_counter_text(:merge_requests, :opened)) + .to include('42') + end + it 'does not take some keys into account in the cache key' do - expect(helper).to receive(:params).and_return({ + expect(issues_finder).to receive(:count_by_state).and_return(opened: 42) + expect(issues_finder).to receive(:params).and_return({ author_id: '11', state: 'foo', sort: 'foo', utf8: 'foo', page: 'foo' }.with_indifferent_access) - expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('<span>Open</span> <span class="badge">42</span>') - expect(helper).to receive(:params).and_return({ + expect(issues_finder).not_to receive(:count_by_state) + expect(issues_finder).to receive(:params).and_return({ author_id: '11', state: 'bar', sort: 'bar', utf8: 'bar', page: 'bar' }.with_indifferent_access) - expect(helper).not_to receive(:issuables_count_for_state) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('<span>Open</span> <span class="badge">42</span>') end it 'does not take params order into account in the cache key' do - expect(helper).to receive(:params).and_return('author_id' => '11', 'state' => 'opened') - expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42) + expect(issues_finder).to receive(:params).and_return('author_id' => '11', 'state' => 'opened') + expect(issues_finder).to receive(:count_by_state).and_return(opened: 42) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('<span>Open</span> <span class="badge">42</span>') - expect(helper).to receive(:params).and_return('state' => 'opened', 'author_id' => '11') - expect(helper).not_to receive(:issuables_count_for_state) + expect(issues_finder).to receive(:params).and_return('state' => 'opened', 'author_id' => '11') + expect(issues_finder).not_to receive(:count_by_state) expect(helper.issuables_state_counter_text(:issues, :opened)) .to eq('<span>Open</span> <span class="badge">42</span>') diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 3fc03324d16..8e056882108 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */ import Cookies from 'js-cookie'; -import AwardsHandler from '~/awards_handler'; +import loadAwardsHandler from '~/awards_handler'; import '~/lib/utils/common_utils'; @@ -26,14 +26,13 @@ import '~/lib/utils/common_utils'; describe('AwardsHandler', function() { preloadFixtures('issues/issue_with_comment.html.raw'); - beforeEach(function() { + beforeEach(function(done) { loadFixtures('issues/issue_with_comment.html.raw'); - awardsHandler = new AwardsHandler; - spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) { - return function(button, url, emoji, cb) { - return cb(); - }; - })(this)); + loadAwardsHandler(true).then((obj) => { + awardsHandler = obj; + spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb()); + done(); + }).catch(fail); let isEmojiMenuBuilt = false; openAndWaitForEmojiMenu = function() { diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 9df92318864..bc13373a27e 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -42,9 +42,6 @@ describe('Issuable output', () => { }).$mount(); }); - afterEach(() => { - }); - it('should render a title/description/edited and update title/description/edited on update', (done) => { vm.poll.options.successCallback({ json() { diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 5ece4ed080b..2c096ed08a8 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -523,6 +523,51 @@ import '~/notes'; }); }); + describe('postComment with Slash commands', () => { + const sampleComment = '/assign @root\n/award :100:'; + const note = { + commands_changes: { + assignee_id: 1, + emoji_award: '100' + }, + errors: { + commands_only: ['Commands applied'] + }, + valid: false + }; + let $form; + let $notesContainer; + + beforeEach(() => { + this.notes = new Notes('', []); + window.gon.current_username = 'root'; + window.gon.current_user_fullname = 'Administrator'; + gl.awardsHandler = { + addAwardToEmojiBar: () => {}, + scrollToAwards: () => {} + }; + gl.GfmAutoComplete = { + dataSources: { + commands: '/root/test-project/autocomplete_sources/commands' + } + }; + $form = $('form.js-main-target-form'); + $notesContainer = $('ul.main-notes-list'); + $form.find('textarea.js-note-text').val(sampleComment); + }); + + it('should remove slash command placeholder when comment with slash commands is done posting', () => { + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough(); + $('.js-comment-button').click(); + + expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown + deferred.resolve(note); + expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed + }); + }); + describe('update comment with script tags', () => { const sampleComment = '<script></script>'; const updatedComment = '<script></script>'; diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index a4bb043f8f1..b7d82c36ddd 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -88,12 +88,12 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do it 'queries the collection on the first call' do expect_any_instance_of(Project).to receive(:default_issues_tracker?).once.and_call_original - expect_any_instance_of(Project).to receive(:issue_reference_pattern).once.and_call_original + expect_any_instance_of(Project).to receive(:external_issue_reference_pattern).once.and_call_original not_cached = reference_filter.call("look for #{reference}", { project: project }) expect_any_instance_of(Project).not_to receive(:default_issues_tracker?) - expect_any_instance_of(Project).not_to receive(:issue_reference_pattern) + expect_any_instance_of(Project).not_to receive(:external_issue_reference_pattern) cached = reference_filter.call("look for #{reference}", { project: project }) diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index e5c1deb338b..a79d365d6c5 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -39,13 +39,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do let(:reference) { "##{issue.iid}" } - it 'ignores valid references when using non-default tracker' do - allow(project).to receive(:default_issues_tracker?).and_return(false) - - exp = act = "Issue #{reference}" - expect(reference_filter(act).to_html).to eq exp - end - it 'links to a valid reference' do doc = reference_filter("Fixed #{reference}") @@ -340,24 +333,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do .to eq({ project => { issue.iid => issue } }) end end - - context 'using an external issue tracker' do - it 'returns a Hash containing the issues per project' do - doc = Nokogiri::HTML.fragment('') - filter = described_class.new(doc, project: project) - - expect(project).to receive(:default_issues_tracker?).and_return(false) - - expect(filter).to receive(:projects_per_reference) - .and_return({ project.path_with_namespace => project }) - - expect(filter).to receive(:references_per_project) - .and_return({ project.path_with_namespace => Set.new([1]) }) - - expect(filter.issues_per_project[project][1]) - .to be_an_instance_of(ExternalIssue) - end - end end describe '.references_in' do diff --git a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb new file mode 100644 index 00000000000..2b8c76f2bb8 --- /dev/null +++ b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb @@ -0,0 +1,33 @@ +require 'rails_helper' + +describe Banzai::Pipeline::GfmPipeline do + describe 'integration between parsing regular and external issue references' do + let(:project) { create(:redmine_project, :public) } + + it 'allows to use shorthand external reference syntax for Redmine' do + markdown = '#12' + + result = described_class.call(markdown, project: project)[:output] + link = result.css('a').first + + expect(link['href']).to eq 'http://redmine/projects/project_name_in_redmine/issues/12' + end + + it 'parses cross-project references to regular issues' do + other_project = create(:empty_project, :public) + issue = create(:issue, project: other_project) + markdown = issue.to_reference(project, full: true) + + result = described_class.call(markdown, project: project)[:output] + link = result.css('a').first + + expect(link['href']).to eq( + Gitlab::Routing.url_helpers.namespace_project_issue_path( + other_project.namespace, + other_project, + issue + ) + ) + end + end +end diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb index 58e1a0c1bc1..acdd23f81f3 100644 --- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb @@ -39,16 +39,6 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do expect(subject.nodes_visible_to_user(user, [link])).to eq([]) end end - - context 'when the project uses an external issue tracker' do - it 'returns all nodes' do - link = double(:link) - - expect(project).to receive(:external_issue_tracker).and_return(true) - - expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) - end - end end describe '#referenced_by' do diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index af0e7855a9b..482f03aa0cc 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -598,8 +598,10 @@ module Ci describe "Image and service handling" do context "when extended docker configuration is used" do it "returns image and service when defined" do - config = YAML.dump({ image: { name: "ruby:2.1" }, - services: ["mysql", { name: "docker:dind", alias: "docker" }], + config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, + services: ["mysql", { name: "docker:dind", alias: "docker", + entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }], before_script: ["pwd"], rspec: { script: "rspec" } }) @@ -614,8 +616,10 @@ module Ci coverage_regex: nil, tag_list: [], options: { - image: { name: "ruby:2.1" }, - services: [{ name: "mysql" }, { name: "docker:dind", alias: "docker" }] + image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, + services: [{ name: "mysql" }, + { name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }] }, allow_failure: false, when: "on_success", @@ -628,8 +632,11 @@ module Ci config = YAML.dump({ image: "ruby:2.1", services: ["mysql"], before_script: ["pwd"], - rspec: { image: { name: "ruby:2.5" }, - services: [{ name: "postgresql", alias: "db-pg" }, "docker:dind"], script: "rspec" } }) + rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, + services: [{ name: "postgresql", alias: "db-pg", + entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }, "docker:dind"], + script: "rspec" } }) config_processor = GitlabCiYamlProcessor.new(config, path) @@ -642,8 +649,10 @@ module Ci coverage_regex: nil, tag_list: [], options: { - image: { name: "ruby:2.5" }, - services: [{ name: "postgresql", alias: "db-pg" }, { name: "docker:dind" }] + image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, + services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }, + { name: "docker:dind" }] }, allow_failure: false, when: "on_success", diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index bca22e39500..1a4d9ed5517 100644 --- a/spec/lib/gitlab/ci/config/entry/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -38,7 +38,7 @@ describe Gitlab::Ci::Config::Entry::Image do end context 'when configuration is a hash' do - let(:config) { { name: 'ruby:2.2', entrypoint: '/bin/sh' } } + let(:config) { { name: 'ruby:2.2', entrypoint: %w(/bin/sh run) } } describe '#value' do it 'returns image hash' do @@ -66,7 +66,7 @@ describe Gitlab::Ci::Config::Entry::Image do describe '#entrypoint' do it "returns image's entrypoint" do - expect(entry.entrypoint).to eq '/bin/sh' + expect(entry.entrypoint).to eq %w(/bin/sh run) end end end diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb index 7202fe525e4..9ebf947a751 100644 --- a/spec/lib/gitlab/ci/config/entry/service_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb @@ -43,7 +43,7 @@ describe Gitlab::Ci::Config::Entry::Service do context 'when configuration is a hash' do let(:config) do - { name: 'postgresql:9.5', alias: 'db', command: 'cmd', entrypoint: '/bin/sh' } + { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run) } end describe '#valid?' do @@ -72,13 +72,13 @@ describe Gitlab::Ci::Config::Entry::Service do describe '#command' do it "returns service's command" do - expect(entry.command).to eq 'cmd' + expect(entry.command).to eq %w(cmd run) end end describe '#entrypoint' do it "returns service's entrypoint" do - expect(entry.entrypoint).to eq '/bin/sh' + expect(entry.entrypoint).to eq %w(/bin/sh run) end end end diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb index 3f279c21865..73518656bde 100644 --- a/spec/lib/gitlab/git/hook_spec.rb +++ b/spec/lib/gitlab/git/hook_spec.rb @@ -4,18 +4,20 @@ require 'fileutils' describe Gitlab::Git::Hook, lib: true do describe "#trigger" do let(:project) { create(:project, :repository) } + let(:repo_path) { project.repository.path } let(:user) { create(:user) } + let(:gl_id) { Gitlab::GlId.gl_id(user) } def create_hook(name) - FileUtils.mkdir_p(File.join(project.repository.path, 'hooks')) - File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f| + FileUtils.mkdir_p(File.join(repo_path, 'hooks')) + File.open(File.join(repo_path, 'hooks', name), 'w', 0755) do |f| f.write('exit 0') end end def create_failing_hook(name) - FileUtils.mkdir_p(File.join(project.repository.path, 'hooks')) - File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f| + FileUtils.mkdir_p(File.join(repo_path, 'hooks')) + File.open(File.join(repo_path, 'hooks', name), 'w', 0755) do |f| f.write(<<-HOOK) echo 'regular message from the hook' echo 'error message from the hook' 1>&2 @@ -27,13 +29,29 @@ describe Gitlab::Git::Hook, lib: true do ['pre-receive', 'post-receive', 'update'].each do |hook_name| context "when triggering a #{hook_name} hook" do context "when the hook is successful" do + let(:hook_path) { File.join(repo_path, 'hooks', hook_name) } + let(:gl_repository) { Gitlab::GlRepository.gl_repository(project, false) } + let(:env) do + { + 'GL_ID' => gl_id, + 'PWD' => repo_path, + 'GL_PROTOCOL' => 'web', + 'GL_REPOSITORY' => gl_repository + } + end + it "returns success with no errors" do create_hook(hook_name) - hook = Gitlab::Git::Hook.new(hook_name, project.repository.path) + hook = Gitlab::Git::Hook.new(hook_name, project) blank = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + if hook_name != 'update' + expect(Open3).to receive(:popen3) + .with(env, hook_path, chdir: repo_path).and_call_original + end + + status, errors = hook.trigger(gl_id, blank, blank, ref) expect(status).to be true expect(errors).to be_blank end @@ -42,11 +60,11 @@ describe Gitlab::Git::Hook, lib: true do context "when the hook is unsuccessful" do it "returns failure with errors" do create_failing_hook(hook_name) - hook = Gitlab::Git::Hook.new(hook_name, project.repository.path) + hook = Gitlab::Git::Hook.new(hook_name, project) blank = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + status, errors = hook.trigger(gl_id, blank, blank, ref) expect(status).to be false expect(errors).to eq("error message from the hook\n") end @@ -56,11 +74,11 @@ describe Gitlab::Git::Hook, lib: true do context "when the hook doesn't exist" do it "returns success with no errors" do - hook = Gitlab::Git::Hook.new('unknown_hook', project.repository.path) + hook = Gitlab::Git::Hook.new('unknown_hook', project) blank = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + status, errors = hook.trigger(gl_id, blank, blank, ref) expect(status).to be true expect(errors).to be_nil end diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb index 168a59e5139..30b6a0d8845 100644 --- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb @@ -34,7 +34,7 @@ describe Gitlab::ImportExport::RepoRestorer, services: true do it 'has the webhooks' do restorer.restore - expect(Gitlab::Git::Hook.new('post-receive', project.repository.path_to_repo)).to exist + expect(Gitlab::Git::Hook.new('post-receive', project)).to exist end end end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index c86f56c55eb..4a1de76f099 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -64,12 +64,12 @@ describe JiraService, models: true do end end - describe '#reference_pattern' do + describe '.reference_pattern' do it_behaves_like 'allows project key on reference pattern' it 'does not allow # on the code' do - expect(subject.reference_pattern.match('#123')).to be_nil - expect(subject.reference_pattern.match('1#23#12')).to be_nil + expect(described_class.reference_pattern.match('#123')).to be_nil + expect(described_class.reference_pattern.match('1#23#12')).to be_nil end end diff --git a/spec/models/project_services/redmine_service_spec.rb b/spec/models/project_services/redmine_service_spec.rb index 6631d9040b1..441b3f896ca 100644 --- a/spec/models/project_services/redmine_service_spec.rb +++ b/spec/models/project_services/redmine_service_spec.rb @@ -31,11 +31,11 @@ describe RedmineService, models: true do end end - describe '#reference_pattern' do + describe '.reference_pattern' do it_behaves_like 'allows project key on reference pattern' it 'does allow # on the reference' do - expect(subject.reference_pattern.match('#123')[:issue]).to eq('123') + expect(described_class.reference_pattern.match('#123')[:issue]).to eq('123') end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 3e984ec7588..c69f0a495db 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -780,7 +780,7 @@ describe Repository, models: true do context 'when pre hooks were successful' do it 'runs without errors' do expect_any_instance_of(GitHooksService).to receive(:execute) - .with(user, project.repository.path_to_repo, old_rev, blank_sha, 'refs/heads/feature') + .with(user, project, old_rev, blank_sha, 'refs/heads/feature') expect { repository.rm_branch(user, 'feature') }.not_to raise_error end @@ -823,12 +823,7 @@ describe Repository, models: true do service = GitHooksService.new expect(GitHooksService).to receive(:new).and_return(service) expect(service).to receive(:execute) - .with( - user, - repository.path_to_repo, - old_rev, - new_rev, - 'refs/heads/feature') + .with(user, project, old_rev, new_rev, 'refs/heads/feature') .and_yield(service).and_return(true) end @@ -1474,9 +1469,9 @@ describe Repository, models: true do it 'passes commit SHA to pre-receive and update hooks,\ and tag SHA to post-receive hook' do - pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', repository.path_to_repo) - update_hook = Gitlab::Git::Hook.new('update', repository.path_to_repo) - post_receive_hook = Gitlab::Git::Hook.new('post-receive', repository.path_to_repo) + pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', project) + update_hook = Gitlab::Git::Hook.new('update', project) + post_receive_hook = Gitlab::Git::Hook.new('post-receive', project) allow(Gitlab::Git::Hook).to receive(:new) .and_return(pre_receive_hook, update_hook, post_receive_hook) diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb index ac7ccfbaab0..213678c27f5 100644 --- a/spec/services/git_hooks_service_spec.rb +++ b/spec/services/git_hooks_service_spec.rb @@ -12,7 +12,6 @@ describe GitHooksService, services: true do @oldrev = sample_commit.parent_id @newrev = sample_commit.id @ref = 'refs/heads/feature' - @repo_path = project.repository.path_to_repo end describe '#execute' do @@ -21,7 +20,7 @@ describe GitHooksService, services: true do hook = double(trigger: [true, nil]) expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) - service.execute(user, @repo_path, @blankrev, @newrev, @ref) { } + service.execute(user, project, @blankrev, @newrev, @ref) { } end end @@ -31,7 +30,7 @@ describe GitHooksService, services: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(user, @repo_path, @blankrev, @newrev, @ref) + service.execute(user, project, @blankrev, @newrev, @ref) end.to raise_error(GitHooksService::PreReceiveError) end end @@ -43,7 +42,7 @@ describe GitHooksService, services: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(user, @repo_path, @blankrev, @newrev, @ref) + service.execute(user, project, @blankrev, @newrev, @ref) end.to raise_error(GitHooksService::PreReceiveError) end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index ca827fc0f39..8e8816870e1 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -401,18 +401,6 @@ describe GitPushService, services: true do expect(SystemNoteService).not_to receive(:cross_reference) execute_service(project, commit_author, @oldrev, @newrev, @ref ) end - - it "doesn't close issues when external issue tracker is in use" do - allow_any_instance_of(Project).to receive(:default_issues_tracker?) - .and_return(false) - external_issue_tracker = double(title: 'My Tracker', issue_path: issue.iid, reference_pattern: project.issue_reference_pattern) - allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(external_issue_tracker) - - # The push still shouldn't create cross-reference notes. - expect do - execute_service(project, commit_author, @oldrev, @newrev, 'refs/heads/hurf' ) - end.not_to change { Note.where(project_id: project.id, system: true).count } - end end context "to non-default branches" do diff --git a/spec/support/issue_tracker_service_shared_example.rb b/spec/support/issue_tracker_service_shared_example.rb index e70b3963d9d..a6ab03cb808 100644 --- a/spec/support/issue_tracker_service_shared_example.rb +++ b/spec/support/issue_tracker_service_shared_example.rb @@ -8,15 +8,15 @@ end RSpec.shared_examples 'allows project key on reference pattern' do |url_attr| it 'allows underscores in the project name' do - expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' + expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' end it 'allows numbers in the project name' do - expect(subject.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234' + expect(described_class.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234' end it 'requires the project name to begin with A-Z' do - expect(subject.reference_pattern.match('3EXT_EXT-1234')).to eq nil - expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' + expect(described_class.reference_pattern.match('3EXT_EXT-1234')).to eq nil + expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' end end diff --git a/spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb b/spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb new file mode 100644 index 00000000000..96c821b26f7 --- /dev/null +++ b/spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb @@ -0,0 +1,9 @@ +shared_examples 'issue sidebar stays collapsed on mobile' do + before do + resize_screen_xs + end + + it 'keeps the sidebar collapsed' do + expect(page).not_to have_css('.right-sidebar.right-sidebar-collapsed') + end +end diff --git a/yarn.lock b/yarn.lock index b902d5235d0..b04eebe60af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -265,6 +265,15 @@ babel-core@^6.22.1, babel-core@^6.23.0: slash "^1.0.0" source-map "^0.5.0" +babel-eslint@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.2.1.tgz#079422eb73ba811e3ca0865ce87af29327f8c52f" + dependencies: + babel-code-frame "^6.22.0" + babel-traverse "^6.23.1" + babel-types "^6.23.0" + babylon "^6.16.1" + babel-generator@^6.18.0, babel-generator@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.23.0.tgz#6b8edab956ef3116f79d8c84c5a3c05f32a74bc5" @@ -816,10 +825,14 @@ babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.22.0, babel-types@^6.23 lodash "^4.2.0" to-fast-properties "^1.0.1" -babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0: +babylon@^6.11.0: version "6.15.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e" +babylon@^6.13.0, babylon@^6.15.0, babylon@^6.16.1: + version "6.16.1" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.16.1.tgz#30c5a22f481978a9e7f8cdfdf496b11d94b404d3" + backo2@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" |