diff options
53 files changed, 1169 insertions, 538 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3cbc826e6db..1fd29fef4f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,6 +94,10 @@ look for [issues with the label `Accepting Merge Requests` and weight < 5][accep These issues will be of reasonable size and challenge, for anyone to start contributing to GitLab. +## Workflow labels + +Labelling issues is described in the [GitLab Inc engineering workflow]. + ## Implement design & UI elements Please see the [UX Guide for GitLab]. @@ -299,10 +303,13 @@ request is as follows: 1. [Generate a changelog entry with `bin/changelog`][changelog] 1. If you are writing documentation, make sure to follow the [documentation styleguide][doc-styleguide] -1. If you have multiple commits please combine them into one commit by - [squashing them][git-squash] +1. If you have multiple commits please combine them into a few logically + organized commits by [squashing them][git-squash] 1. Push the commit(s) to your fork 1. Submit a merge request (MR) to the `master` branch +1. Leave the approvals settings as they are: + 1. Your merge request needs at least 1 approval + 1. You don't have to select any approvers 1. The MR title should describe the change you want to make 1. The MR description should give a motive for your change and the method you used to achieve it. @@ -345,13 +352,31 @@ The ['How to get faster PR reviews' document of Kubernetes](https://github.com/k For examples of feedback on merge requests please look at already [closed merge requests][closed-merge-requests]. If you would like quick feedback -on your merge request feel free to mention one of the Merge Marshalls in the -[core team] or one of the [Merge request coaches](https://about.gitlab.com/team/). +on your merge request feel free to mention someone from the [core team] or one +of the [Merge request coaches][team]. Please ensure that your merge request meets the contribution acceptance criteria. When having your code reviewed and when reviewing merge requests please take the [code review guidelines](doc/development/code_review.md) into account. +### Getting your merge request reviewed, approved, and merged + +There are a few rules to get your merge request accepted: + +1. Your merge request should only be **merged by a [maintainer][team]**. + 1. If your merge request includes only backend changes [^1], it must be + **approved by a [backend maintainer][team]**. + 1. If your merge request includes only frontend changes [^1], it must be + **approved by a [frontend maintainer][team]**. + 1. If your merge request includes frontend and backend changes [^1], it must + be approved by a frontend **and** a backend maintainer. +1. To lower the amount of merge requests maintainers need to review, you can + ask or assign any [reviewers][team] for a first review. + 1. If you need some guidance (e.g. it's your first merge request), feel free + to ask one of the [Merge request coaches][team]. + 1. The reviewer will assign the merge request to a maintainer once the + reviewer is satisfied with the state of the merge request. + ### Contribution acceptance criteria 1. The change is as small as possible @@ -489,6 +514,7 @@ This Code of Conduct is adapted from the [Contributor Covenant][contributor-cove available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/). [core team]: https://about.gitlab.com/core-team/ +[team]: https://about.gitlab.com/team/ [getting-help]: https://about.gitlab.com/getting-help/ [codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq [accepting-mrs-weight]: https://gitlab.com/gitlab-org/gitlab-ce/issues?assignee_id=0&label_name[]=Accepting%20Merge%20Requests&sort=weight_asc @@ -513,3 +539,8 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide" [UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/ [license-finder-doc]: doc/development/licensing.md +[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues + +[^1]: Specs other than JavaScript specs are considered backend code. Haml + changes are considered backend code if they include Ruby code other than just + pure HTML. diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 4667980a960..54836efdf29 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,10 +1,8 @@ /* global Cookies */ -const emojiMap = require('emoji-map'); -const emojiAliases = require('emoji-aliases'); -const glEmoji = require('./behaviors/gl_emoji'); - -const glEmojiTag = glEmoji.glEmojiTag; +import emojiMap from 'emojis/digests.json'; +import emojiAliases from 'emojis/aliases.json'; +import { glEmojiTag } from './behaviors/gl_emoji'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const requestAnimationFrame = window.requestAnimationFrame || @@ -515,4 +513,4 @@ AwardsHandler.prototype.destroy = function destroy() { $('.emoji-menu').remove(); }; -module.exports = AwardsHandler; +export default AwardsHandler; diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js index d1d98c3919f..59741cc9b1a 100644 --- a/app/assets/javascripts/behaviors/gl_emoji.js +++ b/app/assets/javascripts/behaviors/gl_emoji.js @@ -1,11 +1,13 @@ -const installCustomElements = require('document-register-element'); -const emojiMap = require('emoji-map'); -const emojiAliases = require('emoji-aliases'); -const generatedUnicodeSupportMap = require('./gl_emoji/unicode_support_map'); -const spreadString = require('./gl_emoji/spread_string'); +import installCustomElements from 'document-register-element'; +import emojiMap from 'emojis/digests.json'; +import emojiAliases from 'emojis/aliases.json'; +import { getUnicodeSupportMap } from './gl_emoji/unicode_support_map'; +import { isEmojiUnicodeSupported } from './gl_emoji/is_emoji_unicode_supported'; installCustomElements(window); +const generatedUnicodeSupportMap = getUnicodeSupportMap(); + function emojiImageTag(name, src) { return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`; } @@ -55,163 +57,49 @@ function glEmojiTag(inputName, options) { `; } -// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/ -const flagACodePoint = 127462; // parseInt('1F1E6', 16) -const flagZCodePoint = 127487; // parseInt('1F1FF', 16) -function isFlagEmoji(emojiUnicode) { - const cp = emojiUnicode.codePointAt(0); - // Length 4 because flags are made of 2 characters which are surrogate pairs - return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint; -} - -// Chrome <57 renders keycaps oddly -// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294 -// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png -function isKeycapEmoji(emojiUnicode) { - return emojiUnicode.length === 3 && emojiUnicode[2] === '\u20E3'; -} - -// Check for a skin tone variation emoji which aren't always supported -const tone1 = 127995;// parseInt('1F3FB', 16) -const tone5 = 127999;// parseInt('1F3FF', 16) -function isSkinToneComboEmoji(emojiUnicode) { - return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => { - const cp = char.codePointAt(0); - return cp >= tone1 && cp <= tone5; - }); -} - -// macOS supports most skin tone emoji's but -// doesn't support the skin tone versions of horse racing -const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16) -function isHorceRacingSkinToneComboEmoji(emojiUnicode) { - return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint && - isSkinToneComboEmoji(emojiUnicode); -} - -// Check for `family_*`, `kiss_*`, `couple_*` -// For ex. Windows 8.1 Firefox 51.0.1, doesn't support these -const zwj = 8205; // parseInt('200D', 16) -const personStartCodePoint = 128102; // parseInt('1F466', 16) -const personEndCodePoint = 128105; // parseInt('1F469', 16) -function isPersonZwjEmoji(emojiUnicode) { - let hasPersonEmoji = false; - let hasZwj = false; - spreadString(emojiUnicode).forEach((character) => { - const cp = character.codePointAt(0); - if (cp === zwj) { - hasZwj = true; - } else if (cp >= personStartCodePoint && cp <= personEndCodePoint) { - hasPersonEmoji = true; +function installGlEmojiElement() { + const GlEmojiElementProto = Object.create(HTMLElement.prototype); + GlEmojiElementProto.createdCallback = function createdCallback() { + const emojiUnicode = this.textContent.trim(); + const { + name, + unicodeVersion, + fallbackSrc, + fallbackSpriteClass, + } = this.dataset; + + const isEmojiUnicode = this.childNodes && Array.prototype.every.call( + this.childNodes, + childNode => childNode.nodeType === 3, + ); + const hasImageFallback = fallbackSrc && fallbackSrc.length > 0; + const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0; + + if ( + isEmojiUnicode && + !isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion) + ) { + // CSS sprite fallback takes precedence over image fallback + if (hasCssSpriteFalback) { + // 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 = assembleFallbackImageSrc(name); + this.innerHTML = emojiImageTag(name, src); + } } - }); - - return hasPersonEmoji && hasZwj; -} - -// Helper so we don't have to run `isFlagEmoji` twice -// in `isEmojiUnicodeSupported` logic -function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) { - const isFlagResult = isFlagEmoji(emojiUnicode); - return ( - (unicodeSupportMap.flag && isFlagResult) || - !isFlagResult - ); -} - -// Helper so we don't have to run `isSkinToneComboEmoji` twice -// in `isEmojiUnicodeSupported` logic -function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) { - const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode); - return ( - (unicodeSupportMap.skinToneModifier && isSkinToneResult) || - !isSkinToneResult - ); -} - -// Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice -// in `isEmojiUnicodeSupported` logic -function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) { - const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode); - return ( - (unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) || - !isHorseRacingSkinToneResult - ); -} - -// Helper so we don't have to run `isPersonZwjEmoji` twice -// in `isEmojiUnicodeSupported` logic -function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) { - const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode); - return ( - (unicodeSupportMap.personZwj && isPersonZwjResult) || - !isPersonZwjResult - ); -} - -// Takes in a support map and determines whether -// the given unicode emoji is supported on the platform. -// -// Combines all the edge case tests into a one-stop shop method -function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) { - const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome && - unicodeSupportMap.meta.chromeVersion < 57; + }; - // For comments about each scenario, see the comments above each individual respective function - return unicodeSupportMap[unicodeVersion] && - !(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) && - checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) && - checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) && - checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) && - checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode); + document.registerElement('gl-emoji', { + prototype: GlEmojiElementProto, + }); } -const GlEmojiElementProto = Object.create(HTMLElement.prototype); -GlEmojiElementProto.createdCallback = function createdCallback() { - const emojiUnicode = this.textContent.trim(); - const { - name, - unicodeVersion, - fallbackSrc, - fallbackSpriteClass, - } = this.dataset; - - const isEmojiUnicode = this.childNodes && Array.prototype.every.call( - this.childNodes, - childNode => childNode.nodeType === 3, - ); - const hasImageFallback = fallbackSrc && fallbackSrc.length > 0; - const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0; - - if ( - isEmojiUnicode && - !isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion) - ) { - // CSS sprite fallback takes precedence over image fallback - if (hasCssSpriteFalback) { - // 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 = assembleFallbackImageSrc(name); - this.innerHTML = emojiImageTag(name, src); - } - } -}; - -document.registerElement('gl-emoji', { - prototype: GlEmojiElementProto, -}); - -module.exports = { - emojiImageTag, +export { + installGlEmojiElement, glEmojiTag, - isEmojiUnicodeSupported, - isFlagEmoji, - isKeycapEmoji, - isSkinToneComboEmoji, - isHorceRacingSkinToneComboEmoji, - isPersonZwjEmoji, + emojiImageTag, }; diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js new file mode 100644 index 00000000000..5e3c45f7e92 --- /dev/null +++ b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js @@ -0,0 +1,121 @@ +import spreadString from './spread_string'; + +// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/ +const flagACodePoint = 127462; // parseInt('1F1E6', 16) +const flagZCodePoint = 127487; // parseInt('1F1FF', 16) +function isFlagEmoji(emojiUnicode) { + const cp = emojiUnicode.codePointAt(0); + // Length 4 because flags are made of 2 characters which are surrogate pairs + return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint; +} + +// Chrome <57 renders keycaps oddly +// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294 +// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png +function isKeycapEmoji(emojiUnicode) { + return emojiUnicode.length === 3 && emojiUnicode[2] === '\u20E3'; +} + +// Check for a skin tone variation emoji which aren't always supported +const tone1 = 127995;// parseInt('1F3FB', 16) +const tone5 = 127999;// parseInt('1F3FF', 16) +function isSkinToneComboEmoji(emojiUnicode) { + return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => { + const cp = char.codePointAt(0); + return cp >= tone1 && cp <= tone5; + }); +} + +// macOS supports most skin tone emoji's but +// doesn't support the skin tone versions of horse racing +const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16) +function isHorceRacingSkinToneComboEmoji(emojiUnicode) { + return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint && + isSkinToneComboEmoji(emojiUnicode); +} + +// Check for `family_*`, `kiss_*`, `couple_*` +// For ex. Windows 8.1 Firefox 51.0.1, doesn't support these +const zwj = 8205; // parseInt('200D', 16) +const personStartCodePoint = 128102; // parseInt('1F466', 16) +const personEndCodePoint = 128105; // parseInt('1F469', 16) +function isPersonZwjEmoji(emojiUnicode) { + let hasPersonEmoji = false; + let hasZwj = false; + spreadString(emojiUnicode).forEach((character) => { + const cp = character.codePointAt(0); + if (cp === zwj) { + hasZwj = true; + } else if (cp >= personStartCodePoint && cp <= personEndCodePoint) { + hasPersonEmoji = true; + } + }); + + return hasPersonEmoji && hasZwj; +} + +// Helper so we don't have to run `isFlagEmoji` twice +// in `isEmojiUnicodeSupported` logic +function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) { + const isFlagResult = isFlagEmoji(emojiUnicode); + return ( + (unicodeSupportMap.flag && isFlagResult) || + !isFlagResult + ); +} + +// Helper so we don't have to run `isSkinToneComboEmoji` twice +// in `isEmojiUnicodeSupported` logic +function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) { + const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode); + return ( + (unicodeSupportMap.skinToneModifier && isSkinToneResult) || + !isSkinToneResult + ); +} + +// Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice +// in `isEmojiUnicodeSupported` logic +function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) { + const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode); + return ( + (unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) || + !isHorseRacingSkinToneResult + ); +} + +// Helper so we don't have to run `isPersonZwjEmoji` twice +// in `isEmojiUnicodeSupported` logic +function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) { + const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode); + return ( + (unicodeSupportMap.personZwj && isPersonZwjResult) || + !isPersonZwjResult + ); +} + +// Takes in a support map and determines whether +// the given unicode emoji is supported on the platform. +// +// Combines all the edge case tests into a one-stop shop method +function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) { + const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome && + unicodeSupportMap.meta.chromeVersion < 57; + + // For comments about each scenario, see the comments above each individual respective function + return unicodeSupportMap[unicodeVersion] && + !(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) && + checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) && + checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) && + checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) && + checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode); +} + +export { + isEmojiUnicodeSupported, + isFlagEmoji, + isKeycapEmoji, + isSkinToneComboEmoji, + isHorceRacingSkinToneComboEmoji, + isPersonZwjEmoji, +}; diff --git a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js index 2380349c4fa..327764ec6e9 100644 --- a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js +++ b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js @@ -47,4 +47,4 @@ function spreadString(str) { return arr; } -module.exports = spreadString; +export default spreadString; diff --git a/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js b/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js index f31716d4c07..aa522e20c36 100644 --- a/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js +++ b/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js @@ -68,7 +68,7 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche // See 32px, https://i.imgur.com/htY6Zym.png // See 16px, https://i.imgur.com/FPPsIF8.png const fontSize = 16; -function testUnicodeSupportMap(testMap) { +function generateUnicodeSupportMap(testMap) { const testMapKeys = Object.keys(testMap); const numTestEntries = testMapKeys .reduce((list, testKey) => list.concat(testMap[testKey]), []).length; @@ -138,17 +138,24 @@ function testUnicodeSupportMap(testMap) { return resultMap; } -let unicodeSupportMap; -const userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent'); -try { - unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map')); -} catch (err) { - // swallow -} -if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) { - unicodeSupportMap = testUnicodeSupportMap(unicodeSupportTestMap); - window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent); - window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap)); +function getUnicodeSupportMap() { + let unicodeSupportMap; + const userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent'); + try { + unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map')); + } catch (err) { + // swallow + } + if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) { + unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap); + window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent); + window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap)); + } + + return unicodeSupportMap; } -module.exports = unicodeSupportMap; +export { + getUnicodeSupportMap, + generateUnicodeSupportMap, +}; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 017980271b1..7b9b9123c31 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -39,6 +39,7 @@ import Issue from './issue'; import BindInOut from './behaviors/bind_in_out'; import GroupsList from './groups_list'; import ProjectsList from './projects_list'; +import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; const ShortcutsBlob = require('./shortcuts_blob'); const UserCallout = require('./user_callout'); @@ -181,7 +182,7 @@ const UserCallout = require('./user_callout'); shortcut_handler = new ShortcutsNavigation(); break; case 'projects:commit:pipelines': - new gl.MiniPipelineGraph({ + new MiniPipelineGraph({ container: '.js-pipeline-table', }).bindEvents(); break; diff --git a/app/assets/javascripts/extensions/string.js b/app/assets/javascripts/extensions/string.js index fe23be0bbc1..ae9662444b0 100644 --- a/app/assets/javascripts/extensions/string.js +++ b/app/assets/javascripts/extensions/string.js @@ -1,2 +1,2 @@ -require('string.prototype.codepointat'); -require('string.fromcodepoint'); +import 'string.prototype.codepointat'; +import 'string.fromcodepoint'; diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 1bc04a5ad96..4f7ce1fa197 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -1,10 +1,8 @@ /* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */ -const emojiMap = require('emoji-map'); -const emojiAliases = require('emoji-aliases'); -const glEmoji = require('./behaviors/gl_emoji'); - -const glEmojiTag = glEmoji.glEmojiTag; +import emojiMap from 'emojis/digests.json'; +import emojiAliases from 'emojis/aliases.json'; +import { glEmojiTag } from '~/behaviors/gl_emoji'; // Creates the variables for setting up GFM auto-completion (function() { diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 1bc81d2e4a4..09c4261b318 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -66,6 +66,13 @@ return results; })()).join('&'); }; + w.gl.utils.removeParams = (params) => { + const url = new URL(window.location.href); + params.forEach((param) => { + url.search = w.gl.utils.removeParamQueryString(url.search, param); + }); + return url.href; + }; w.gl.utils.getLocationHash = function(url) { var hashIndex; if (typeof url === 'undefined') { diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 79164edff0e..689a6c3a93a 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import */ +/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */ /* global bp */ /* global Cookies */ /* global Flash */ @@ -13,19 +13,20 @@ import Dropzone from 'dropzone'; import Sortable from 'vendor/Sortable'; // libraries with import side-effects -require('mousetrap'); -require('mousetrap/plugins/pause/mousetrap-pause'); -require('vendor/fuzzaldrin-plus'); -require('es6-promise').polyfill(); +import 'mousetrap'; +import 'mousetrap/plugins/pause/mousetrap-pause'; +import 'vendor/fuzzaldrin-plus'; +import promisePolyfill from 'es6-promise'; // extensions -require('./extensions/string'); -require('./extensions/array'); -require('./extensions/custom_event'); -require('./extensions/element'); -require('./extensions/jquery'); -require('./extensions/object'); -require('es6-promise').polyfill(); +import './extensions/string'; +import './extensions/array'; +import './extensions/custom_event'; +import './extensions/element'; +import './extensions/jquery'; +import './extensions/object'; + +promisePolyfill.polyfill(); // expose common libraries as globals (TODO: remove these) window.jQuery = jQuery; @@ -37,174 +38,171 @@ window.Dropzone = Dropzone; window.Sortable = Sortable; // shortcuts -require('./shortcuts'); -require('./shortcuts_navigation'); -require('./shortcuts_dashboard_navigation'); -require('./shortcuts_issuable'); -require('./shortcuts_network'); +import './shortcuts'; +import './shortcuts_blob'; +import './shortcuts_dashboard_navigation'; +import './shortcuts_navigation'; +import './shortcuts_find_file'; +import './shortcuts_issuable'; +import './shortcuts_network'; // behaviors -require('./behaviors/autosize'); -require('./behaviors/details_behavior'); -require('./behaviors/quick_submit'); -require('./behaviors/requires_input'); -require('./behaviors/toggler_behavior'); -require('./behaviors/bind_in_out'); +import './behaviors/autosize'; +import './behaviors/details_behavior'; +import './behaviors/quick_submit'; +import './behaviors/requires_input'; +import './behaviors/toggler_behavior'; +import './behaviors/bind_in_out'; +import { installGlEmojiElement } from './behaviors/gl_emoji'; +installGlEmojiElement(); // blob -require('./blob/blob_ci_yaml'); -require('./blob/blob_dockerfile_selector'); -require('./blob/blob_dockerfile_selectors'); -require('./blob/blob_file_dropzone'); -require('./blob/blob_gitignore_selector'); -require('./blob/blob_gitignore_selectors'); -require('./blob/blob_license_selector'); -require('./blob/blob_license_selectors'); -require('./blob/template_selector'); +import './blob/blob_ci_yaml'; +import './blob/blob_dockerfile_selector'; +import './blob/blob_dockerfile_selectors'; +import './blob/blob_file_dropzone'; +import './blob/blob_gitignore_selector'; +import './blob/blob_gitignore_selectors'; +import './blob/blob_license_selector'; +import './blob/blob_license_selectors'; +import './blob/template_selector'; // templates -require('./templates/issuable_template_selector'); -require('./templates/issuable_template_selectors'); +import './templates/issuable_template_selector'; +import './templates/issuable_template_selectors'; // commit -require('./commit/file.js'); -require('./commit/image_file.js'); +import './commit/file'; +import './commit/image_file'; // lib/utils -require('./lib/utils/animate'); -require('./lib/utils/bootstrap_linked_tabs'); -require('./lib/utils/common_utils'); -require('./lib/utils/datetime_utility'); -require('./lib/utils/notify'); -require('./lib/utils/pretty_time'); -require('./lib/utils/text_utility'); -require('./lib/utils/type_utility'); -require('./lib/utils/url_utility'); +import './lib/utils/animate'; +import './lib/utils/bootstrap_linked_tabs'; +import './lib/utils/common_utils'; +import './lib/utils/datetime_utility'; +import './lib/utils/notify'; +import './lib/utils/pretty_time'; +import './lib/utils/text_utility'; +import './lib/utils/type_utility'; +import './lib/utils/url_utility'; // u2f -require('./u2f/authenticate'); -require('./u2f/error'); -require('./u2f/register'); -require('./u2f/util'); +import './u2f/authenticate'; +import './u2f/error'; +import './u2f/register'; +import './u2f/util'; // droplab -require('./droplab/droplab'); -require('./droplab/droplab_ajax'); -require('./droplab/droplab_ajax_filter'); -require('./droplab/droplab_filter'); +import './droplab/droplab'; +import './droplab/droplab_ajax'; +import './droplab/droplab_ajax_filter'; +import './droplab/droplab_filter'; // everything else -require('./abuse_reports'); -require('./activities'); -require('./admin'); -require('./ajax_loading_spinner'); -require('./api'); -require('./aside'); -require('./autosave'); -const AwardsHandler = require('./awards_handler'); -require('./breakpoints'); -require('./broadcast_message'); -require('./build'); -require('./build_artifacts'); -require('./build_variables'); -require('./ci_lint_editor'); -require('./commit'); -require('./commits'); -require('./compare'); -require('./compare_autocomplete'); -require('./confirm_danger_modal'); -require('./copy_as_gfm'); -require('./copy_to_clipboard'); -require('./create_label'); -require('./diff'); -require('./dispatcher'); -require('./dropzone_input'); -require('./due_date_select'); -require('./files_comment_button'); -require('./flash'); -require('./gfm_auto_complete'); -require('./gl_dropdown'); -require('./gl_field_error'); -require('./gl_field_errors'); -require('./gl_form'); -require('./group_avatar'); -require('./group_label_subscription'); -require('./groups_select'); -require('./header'); -require('./importer_status'); -require('./issuable'); -require('./issuable_context'); -require('./issuable_form'); -require('./issue'); -require('./issue_status_select'); -require('./issues_bulk_assignment'); -require('./label_manager'); -require('./labels'); -require('./labels_select'); -require('./layout_nav'); -require('./line_highlighter'); -require('./logo'); -require('./member_expiration_date'); -require('./members'); -require('./merge_request'); -require('./merge_request_tabs'); -require('./merge_request_widget'); -require('./merged_buttons'); -require('./milestone'); -require('./milestone_select'); -require('./mini_pipeline_graph_dropdown'); -require('./namespace_select'); -require('./new_branch_form'); -require('./new_commit_form'); -require('./notes'); -require('./notifications_dropdown'); -require('./notifications_form'); -require('./pager'); -require('./pipelines'); -require('./preview_markdown'); -require('./project'); -require('./project_avatar'); -require('./project_find_file'); -require('./project_fork'); -require('./project_import'); -require('./project_label_subscription'); -require('./project_new'); -require('./project_select'); -require('./project_show'); -require('./project_variables'); -require('./projects_list'); -require('./render_gfm'); -require('./render_math'); -require('./right_sidebar'); -require('./search'); -require('./search_autocomplete'); -require('./shortcuts'); -require('./shortcuts_blob'); -require('./shortcuts_dashboard_navigation'); -require('./shortcuts_find_file'); -require('./shortcuts_issuable'); -require('./shortcuts_navigation'); -require('./shortcuts_network'); -require('./signin_tabs_memoizer'); -require('./single_file_diff'); -require('./smart_interval'); -require('./snippets_list'); -require('./star'); -require('./subbable_resource'); -require('./subscription'); -require('./subscription_select'); -require('./syntax_highlight'); -require('./task_list'); -require('./todos'); -require('./tree'); -require('./user'); -require('./user_tabs'); -require('./username_validator'); -require('./users_select'); -require('./version_check_image'); -require('./visibility_select'); -require('./wikis'); -require('./zen_mode'); +import './abuse_reports'; +import './activities'; +import './admin'; +import './ajax_loading_spinner'; +import './api'; +import './aside'; +import './autosave'; +import AwardsHandler from './awards_handler'; +import './breakpoints'; +import './broadcast_message'; +import './build'; +import './build_artifacts'; +import './build_variables'; +import './ci_lint_editor'; +import './commit'; +import './commits'; +import './compare'; +import './compare_autocomplete'; +import './confirm_danger_modal'; +import './copy_as_gfm'; +import './copy_to_clipboard'; +import './create_label'; +import './diff'; +import './dispatcher'; +import './dropzone_input'; +import './due_date_select'; +import './files_comment_button'; +import './flash'; +import './gfm_auto_complete'; +import './gl_dropdown'; +import './gl_field_error'; +import './gl_field_errors'; +import './gl_form'; +import './group_avatar'; +import './group_label_subscription'; +import './groups_select'; +import './header'; +import './importer_status'; +import './issuable'; +import './issuable_context'; +import './issuable_form'; +import './issue'; +import './issue_status_select'; +import './issues_bulk_assignment'; +import './label_manager'; +import './labels'; +import './labels_select'; +import './layout_nav'; +import './line_highlighter'; +import './logo'; +import './member_expiration_date'; +import './members'; +import './merge_request'; +import './merge_request_tabs'; +import './merge_request_widget'; +import './merged_buttons'; +import './milestone'; +import './milestone_select'; +import './mini_pipeline_graph_dropdown'; +import './namespace_select'; +import './new_branch_form'; +import './new_commit_form'; +import './notes'; +import './notifications_dropdown'; +import './notifications_form'; +import './pager'; +import './pipelines'; +import './preview_markdown'; +import './project'; +import './project_avatar'; +import './project_find_file'; +import './project_fork'; +import './project_import'; +import './project_label_subscription'; +import './project_new'; +import './project_select'; +import './project_show'; +import './project_variables'; +import './projects_list'; +import './render_gfm'; +import './render_math'; +import './right_sidebar'; +import './search'; +import './search_autocomplete'; +import './signin_tabs_memoizer'; +import './single_file_diff'; +import './smart_interval'; +import './snippets_list'; +import './star'; +import './subbable_resource'; +import './subscription'; +import './subscription_select'; +import './syntax_highlight'; +import './task_list'; +import './todos'; +import './tree'; +import './user'; +import './user_tabs'; +import './username_validator'; +import './users_select'; +import './version_check_image'; +import './visibility_select'; +import './wikis'; +import './zen_mode'; (function () { document.addEventListener('beforeunload', function () { diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js index 5f1bd474a0c..66cc270ab4d 100644 --- a/app/assets/javascripts/merge_request_widget.js +++ b/app/assets/javascripts/merge_request_widget.js @@ -3,7 +3,8 @@ /* global notifyPermissions */ /* global merge_request_widget */ -require('./smart_interval'); +import './smart_interval'; +import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; ((global) => { var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; @@ -285,7 +286,7 @@ require('./smart_interval'); }; MergeRequestWidget.prototype.initMiniPipelineGraph = function() { - new gl.MiniPipelineGraph({ + new MiniPipelineGraph({ container: '.js-pipeline-inline-mr-widget-graph:visible', }).bindEvents(); }; diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js index 2145e531331..9c58c465001 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js @@ -15,81 +15,96 @@ * <div class="js-builds-dropdown-container dropdown-menu"></div> * </div> */ -(() => { - class MiniPipelineGraph { - constructor(opts = {}) { - this.container = opts.container || ''; - this.dropdownListSelector = '.js-builds-dropdown-container'; - this.getBuildsList = this.getBuildsList.bind(this); - } - /** - * Adds the event listener when the dropdown is opened. - * All dropdown events are fired at the .dropdown-menu's parent element. - */ - bindEvents() { - $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList); - } +export default class MiniPipelineGraph { + constructor(opts = {}) { + this.container = opts.container || ''; + this.dropdownListSelector = '.js-builds-dropdown-container'; + this.getBuildsList = this.getBuildsList.bind(this); + } + + /** + * Adds the event listener when the dropdown is opened. + * All dropdown events are fired at the .dropdown-menu's parent element. + */ + bindEvents() { + $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList); + } - /** - * For the clicked stage, renders the given data in the dropdown list. - * - * @param {HTMLElement} stageContainer - * @param {Object} data - */ - renderBuildsList(stageContainer, data) { - const dropdownContainer = stageContainer.parentElement.querySelector( - `${this.dropdownListSelector} .js-builds-dropdown-list`, - ); + /** + * When the user right clicks or cmd/ctrl + click in the job name + * the dropdown should not be closed and the link should open in another tab, + * so we stop propagation of the click event inside the dropdown. + * + * Since this component is rendered multiple times per page we need to guarantee we only + * target the click event of this component. + */ + stopDropdownClickPropagation() { + $(document).on( + 'click', + `${this.container} .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item`, + (e) => { + e.stopPropagation(); + }, + ); + } - dropdownContainer.innerHTML = data; - } + /** + * For the clicked stage, renders the given data in the dropdown list. + * + * @param {HTMLElement} stageContainer + * @param {Object} data + */ + renderBuildsList(stageContainer, data) { + const dropdownContainer = stageContainer.parentElement.querySelector( + `${this.dropdownListSelector} .js-builds-dropdown-list`, + ); - /** - * For the clicked stage, gets the list of builds. - * - * All dropdown events have a relatedTarget property, - * whose value is the toggling anchor element. - * - * @param {Object} e bootstrap dropdown event - * @return {Promise} - */ - getBuildsList(e) { - const button = e.relatedTarget; - const endpoint = button.dataset.stageEndpoint; + dropdownContainer.innerHTML = data; + } - return $.ajax({ - dataType: 'json', - type: 'GET', - url: endpoint, - beforeSend: () => { - this.renderBuildsList(button, ''); - this.toggleLoading(button); - }, - success: (data) => { - this.toggleLoading(button); - this.renderBuildsList(button, data.html); - }, - error: () => { - this.toggleLoading(button); - new Flash('An error occurred while fetching the builds.', 'alert'); - }, - }); - } + /** + * For the clicked stage, gets the list of builds. + * + * All dropdown events have a relatedTarget property, + * whose value is the toggling anchor element. + * + * @param {Object} e bootstrap dropdown event + * @return {Promise} + */ + getBuildsList(e) { + const button = e.relatedTarget; + const endpoint = button.dataset.stageEndpoint; - /** - * Toggles the visibility of the loading icon. - * - * @param {HTMLElement} stageContainer - * @return {type} - */ - toggleLoading(stageContainer) { - stageContainer.parentElement.querySelector( - `${this.dropdownListSelector} .js-builds-dropdown-loading`, - ).classList.toggle('hidden'); - } + return $.ajax({ + dataType: 'json', + type: 'GET', + url: endpoint, + beforeSend: () => { + this.renderBuildsList(button, ''); + this.toggleLoading(button); + }, + success: (data) => { + this.toggleLoading(button); + this.renderBuildsList(button, data.html); + this.stopDropdownClickPropagation(); + }, + error: () => { + this.toggleLoading(button); + new Flash('An error occurred while fetching the builds.', 'alert'); + }, + }); } - window.gl = window.gl || {}; - window.gl.MiniPipelineGraph = MiniPipelineGraph; -})(); + /** + * Toggles the visibility of the loading icon. + * + * @param {HTMLElement} stageContainer + * @return {type} + */ + toggleLoading(stageContainer) { + stageContainer.parentElement.querySelector( + `${this.dropdownListSelector} .js-builds-dropdown-loading`, + ).classList.toggle('hidden'); + } +} diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index eeab69da941..47cc34e7a20 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,12 +1,14 @@ /* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape */ /* global Flash */ /* global Autosave */ +/* global Cookies */ /* global ResolveService */ /* global mrRefreshWidgetUrl */ require('./autosave'); window.autosize = require('vendor/autosize'); window.Dropzone = require('dropzone'); +window.Cookies = require('js-cookie'); require('./dropzone_input'); require('./gfm_auto_complete'); require('vendor/jquery.caret'); // required by jquery.atwho @@ -42,7 +44,6 @@ require('./task_list'); this.notes_url = notes_url; this.note_ids = note_ids; this.last_fetched_at = last_fetched_at; - this.view = view; this.noteable_url = document.URL; this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge")); this.basePollingInterval = 15000; @@ -57,6 +58,7 @@ require('./task_list'); selector: '.notes' }); this.collapseLongCommitList(); + this.setViewType(view); // We are in the Merge Requests page so we need another edit form for Changes tab if (gl.utils.getPagePath(1) === 'merge_requests') { @@ -65,6 +67,10 @@ require('./task_list'); } } + Notes.prototype.setViewType = function(view) { + this.view = Cookies.get('diff_view') || view; + }; + Notes.prototype.addBinding = function() { // add note to UI after creation $(document).on("ajax:success", ".js-main-target-form", this.addNote); @@ -302,7 +308,7 @@ require('./task_list'); }; Notes.prototype.isParallelView = function() { - return this.view === 'parallel'; + return Cookies.get('diff_view') === 'parallel'; }; /* diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index e35cf6d295e..5f6bc902cf8 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -1,11 +1,15 @@ +require('~/lib/utils/common_utils'); +require('~/lib/utils/url_utility'); + (() => { const ENDLESS_SCROLL_BOTTOM_PX = 400; const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000; const Pager = { init(limit = 0, preload = false, disable = false, callback = $.noop) { + this.url = $('.content_list').data('href') || gl.utils.removeParams(['limit', 'offset']); this.limit = limit; - this.offset = this.limit; + this.offset = parseInt(gl.utils.getParameterByName('offset'), 10) || this.limit; this.disable = disable; this.callback = callback; this.loading = $('.loading').first(); @@ -20,7 +24,7 @@ this.loading.show(); $.ajax({ type: 'GET', - url: $('.content_list').data('href') || window.location.href, + url: this.url, data: `limit=${this.limit}&offset=${this.offset}`, dataType: 'json', error: () => this.loading.hide(), diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index d7a45bacd35..b79ca034c5b 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -18,8 +18,7 @@ class AutocompleteController < ApplicationController if params[:search].blank? # Include current user if available to filter by "Me" if params[:current_user].present? && current_user - @users = @users.where.not(id: current_user.id) - @users = [current_user, *@users] + @users = [current_user, *@users].uniq end if params[:author_id].present? diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 17cb1d5be24..f9d798d0455 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -13,7 +13,8 @@ class Projects::ServicesController < Projects::ApplicationController end def update - if @service.update_attributes(service_params[:service]) + @service.assign_attributes(service_params[:service]) + if @service.save(context: :manual_change) redirect_to( edit_namespace_project_service_path(@project.namespace, @project, @service.to_param), notice: 'Successfully updated.' diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 67206415f7b..8a5a9aa4adb 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -144,7 +144,7 @@ module Ci status_sql = statuses.latest.where('stage=sg.stage').status_sql - warnings_sql = statuses.latest.select('COUNT(*) > 0') + warnings_sql = statuses.latest.select('COUNT(*)') .where('stage=sg.stage').failed_but_allowed.to_sql stages_with_statuses = CommitStatus.from(stages_query, :sg) diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index ca74c91b062..e7d6b17d445 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -46,10 +46,10 @@ module Ci end def has_warnings? - if @warnings.nil? - statuses.latest.failed_but_allowed.any? + if @warnings.is_a?(Integer) + @warnings > 0 else - @warnings + statuses.latest.failed_but_allowed.any? end end end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 37c727b5d9f..3cf4c67d7e7 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -63,6 +63,7 @@ module Issuable scope :authored, ->(user) { where(author_id: user) } scope :assigned_to, ->(u) { where(assignee_id: u.id)} scope :recent, -> { reorder(id: :desc) } + scope :order_position_asc, -> { reorder(position: :asc) } scope :assigned, -> { where("assignee_id IS NOT NULL") } scope :unassigned, -> { where("assignee_id IS NULL") } scope :of_projects, ->(ids) { where(project_id: ids) } @@ -144,6 +145,7 @@ module Issuable when 'downvotes_desc' then order_downvotes_desc when 'upvotes_desc' then order_upvotes_desc when 'priority' then order_labels_priority(excluded_labels: excluded_labels) + when 'position_asc' then order_position_asc else order_by(method) end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 9e65fdbf9d6..50435b67eda 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -1,4 +1,6 @@ class IssueTrackerService < Service + validate :one_issue_tracker, if: :activated?, on: :manual_change + default_value_for :category, 'issue_tracker' # Pattern used to extract links from comments @@ -92,4 +94,13 @@ class IssueTrackerService < Service def issues_tracker Gitlab.config.issues_tracker[to_param] end + + def one_issue_tracker + return if template? + return if project.blank? + + if project.services.external_issue_trackers.where.not(id: id).any? + errors.add(:base, 'Another issue tracker is already in use. Only one issue tracker service can be active at a time') + end + end end diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 30e63d991bb..a2f6a7ab1cb 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -4,7 +4,7 @@ .devise-errors = devise_error_messages! .form-group - = f.label :name + = f.label :name, 'Full name' = f.text_field :name, class: "form-control top", required: true, title: "This field is required." .username.form-group = f.label :username diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index ed279cfe168..62135d3ae32 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -14,13 +14,13 @@ %td.new_line.diff-line-num %td.line_content.match= line.text - else - %td.old_line.diff-line-num.js-avatar-container{ class: type, data: { linenumber: line.old_pos } } + %td.old_line.diff-line-num{ class: [type, ("js-avatar-container" if !plain)], data: { linenumber: line.old_pos } } - link_text = type == "new" ? " " : line.old_pos - if plain = link_text - else %a{ href: "##{line_code}", data: { linenumber: link_text } } - - if discussion && !plain + - if discussion && discussion.resolvable? && !plain %diff-note-avatars{ "discussion-id" => discussion.id } %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } - link_text = type == "old" ? " " : line.new_pos diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 6448748113b..e7758c8bdfa 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -20,7 +20,7 @@ - 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 } } %a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } } - - if discussion_left + - 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) - else @@ -39,7 +39,7 @@ - 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 } } %a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } } - - if discussion_right + - 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) - else diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index cb92b2e97a7..70470c83c51 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -62,24 +62,25 @@ - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) .row-content-block{ class: (is_footer ? "footer-block" : "middle-block") } - - if issuable.new_record? - = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' - - else - = form.submit 'Save changes', class: 'btn btn-save' + .pull-right + - if issuable.new_record? + = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel' + - else + - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project) + = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" }, method: :delete, class: 'btn btn-danger btn-grouped' + = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' + + %span.append-right-10 + - if issuable.new_record? + = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' + - else + = form.submit 'Save changes', class: 'btn btn-save' - if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project)) - .inline.prepend-left-10 + .inline.prepend-top-10 Please review the %strong= link_to('contribution guidelines', guide_url) for this project. - - if issuable.new_record? - = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel' - - else - .pull-right - - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project) - = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" }, - method: :delete, class: 'btn btn-danger btn-grouped' - = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' = form.hidden_field :lock_version diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 32128f3b3dc..f8123846596 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -14,18 +14,18 @@ .scroll-container %ul.tokens-container.list-unstyled %li.input-token - %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]), 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) } + %input.form-control.filtered-search{ placeholder: 'Search or filter results...', data: { id: 'filtered-search', 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } } = icon('filter') %button.clear-search.hidden{ type: 'button' } = icon('times') #js-dropdown-hint.dropdown-menu.hint-dropdown - %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-action' => 'submit' } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { action: 'submit' } } %button.btn.btn-link = icon('search') %span Keep typing and press Enter - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link -# Encapsulate static class name `{{icon}}` inside #{} to bypass @@ -36,50 +36,50 @@ %span.js-filter-tag.dropdown-light-content {{tag}} #js-dropdown-author.dropdown-menu{ data: { icon: 'pencil', hint: 'author', tag: '@author' } } - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user - %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } + %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } } .dropdown-user-details %span {{name}} %span.dropdown-light-content @{{username}} #js-dropdown-assignee.dropdown-menu{ data: { icon: 'user', hint: 'assignee', tag: '@assignee' } } - %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value' => 'none' } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'none' } } %button.btn.btn-link No Assignee %li.divider - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user - %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } + %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } } .dropdown-user-details %span {{name}} %span.dropdown-light-content @{{username}} #js-dropdown-milestone.dropdown-menu{ data: { icon: 'clock-o', hint: 'milestone', tag: '%milestone' } } - %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value' => 'none' } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'none' } } %button.btn.btn-link No Milestone - %li.filter-dropdown-item{ 'data-value' => 'upcoming' } + %li.filter-dropdown-item{ data: { value: 'upcoming' } } %button.btn.btn-link Upcoming %li.divider - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link.js-data-value {{title}} #js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label' } } - %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value' => 'none' } + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'none' } } %button.btn.btn-link No Label %li.divider - %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link %span.dropdown-label-box{ style: 'background: {{color}}' } diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml index a93cbd1041f..8af3bd597c5 100644 --- a/app/views/shared/milestones/_issuables.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -13,6 +13,6 @@ - class_prefix = dom_class(issuables).pluralize %ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id } = render partial: 'shared/milestones/issuable', - collection: issuables.sort_by(&:position), + collection: issuables.order_position_asc, as: :issuable, locals: { show_project_name: show_project_name, show_full_project_name: show_full_project_name } diff --git a/changelogs/unreleased/24166-close-builds-dropdown.yml b/changelogs/unreleased/24166-close-builds-dropdown.yml new file mode 100644 index 00000000000..c57ffed6b45 --- /dev/null +++ b/changelogs/unreleased/24166-close-builds-dropdown.yml @@ -0,0 +1,4 @@ +--- +title: Prevent builds dropdown to close when the user clicks in a build +merge_request: +author: diff --git a/changelogs/unreleased/28030-infinite-offset.yml b/changelogs/unreleased/28030-infinite-offset.yml new file mode 100644 index 00000000000..6f4082d7684 --- /dev/null +++ b/changelogs/unreleased/28030-infinite-offset.yml @@ -0,0 +1,4 @@ +--- +title: allow offset query parameter for infinite list pages +merge_request: +author: diff --git a/changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml b/changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml new file mode 100644 index 00000000000..0177394aa0f --- /dev/null +++ b/changelogs/unreleased/28874-fix-milestone-issues-position-order-in-api.yml @@ -0,0 +1,4 @@ +--- +title: Order milestone issues by position ascending in api +merge_request: 9635 +author: George Andrinopoulos diff --git a/changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml b/changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml new file mode 100644 index 00000000000..f869249c22b --- /dev/null +++ b/changelogs/unreleased/29014-create-issue-form-buttons-misaligned-on-mobile.yml @@ -0,0 +1,4 @@ +--- +title: Fix create issue form buttons are misaligned on mobile +merge_request: 9706 +author: TM Lee diff --git a/changelogs/unreleased/29209-sign-up-form-name.yml b/changelogs/unreleased/29209-sign-up-form-name.yml new file mode 100644 index 00000000000..e8e3a71f875 --- /dev/null +++ b/changelogs/unreleased/29209-sign-up-form-name.yml @@ -0,0 +1,4 @@ +--- +title: Change label for name on sign up form +merge_request: +author: diff --git a/changelogs/unreleased/adam-prevent-two-issue-trackers.yml b/changelogs/unreleased/adam-prevent-two-issue-trackers.yml new file mode 100644 index 00000000000..307b7ec7359 --- /dev/null +++ b/changelogs/unreleased/adam-prevent-two-issue-trackers.yml @@ -0,0 +1,4 @@ +--- +title: Prevent more than one issue tracker to be active for the same project +merge_request: +author: luisdgs19 diff --git a/changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml b/changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml new file mode 100644 index 00000000000..6365b1a1910 --- /dev/null +++ b/changelogs/unreleased/fix-gb-passed-with-warnings-status-on-mysql.yml @@ -0,0 +1,4 @@ +--- +title: Fix "passed with warnings" stage status on MySQL installations +merge_request: 9802 +author: diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 720df0cac2d..b4f6cba1833 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -441,6 +441,16 @@ production: &base shared: # path: /mnt/gitlab # Default: shared + # Gitaly settings + gitaly: + # The socket_path setting is optional and obsolete. When this is set + # GitLab assumes it can reach a Gitaly services via a Unix socket at + # this path. When this is commented out GitLab will not use Gitaly. + # + # This setting is obsolete because we expect it to be moved under + # repositories/storages in GitLab 9.1. + # + # socket_path: tmp/sockets/gitaly.socket # # 4. Advanced settings diff --git a/config/webpack.config.js b/config/webpack.config.js index ff5f1412261..8e2b11a4145 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -133,8 +133,7 @@ var config = { extensions: ['.js', '.es6', '.js.es6'], alias: { '~': path.join(ROOT_PATH, 'app/assets/javascripts'), - 'emoji-map$': path.join(ROOT_PATH, 'fixtures/emojis/digests.json'), - 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'), + 'emojis': path.join(ROOT_PATH, 'fixtures/emojis'), 'empty_states': path.join(ROOT_PATH, 'app/views/shared/empty_states'), 'icons': path.join(ROOT_PATH, 'app/views/shared/icons'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md index 463715e48ca..f6f50e2c571 100644 --- a/doc/administration/pages/source.md +++ b/doc/administration/pages/source.md @@ -17,14 +17,17 @@ Pages to the latest supported version. ## Prerequisites -Before proceeding with the Pages configuration, you will need to: - -1. Have a separate domain under which the GitLab Pages will be served. In this - document we assume that to be `example.io`. -1. Configure a **wildcard DNS record**. -1. (Optional) Have a **wildcard certificate** for that domain if you decide to - serve Pages under HTTPS. -1. (Optional but recommended) Enable [Shared runners](../../ci/runners/README.md) +Before proceeding with the Pages configuration, make sure that: + +1. You have a separate domain under which GitLab Pages will be served. In + this document we assume that to be `example.io`. +1. You have configured a **wildcard DNS record** for that domain. +1. You have installed the `zip` and `unzip` packages in the same server that + GitLab is installed since they are needed to compress/uncompress the + Pages artifacts. +1. (Optional) You have a **wildcard certificate** for the Pages domain if you + decide to serve Pages (`*.example.io`) under HTTPS. +1. (Optional but recommended) You have configured and enabled the [Shared Runners][] so that your users don't have to bring their own. ### DNS configuration @@ -390,3 +393,4 @@ than GitLab to prevent XSS attacks. [reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure [restart]: ../restart_gitlab.md#installations-from-source [gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4 +[shared runners]: ../../ci/runners/README.md diff --git a/doc/development/frontend.md b/doc/development/frontend.md index 9ba820eaee5..d646de7c54a 100644 --- a/doc/development/frontend.md +++ b/doc/development/frontend.md @@ -16,6 +16,22 @@ minification, and compression of our assets. [jQuery][jquery] is used throughout the application's JavaScript, with [Vue.js][vue] for particularly advanced, dynamic elements. +### Architecture + +The Frontend Architect is an expert who makes high-level frontend design choices +and decides on technical standards, including coding standards, and frameworks. + +When you are assigned a new feature that requires architectural design, +make sure it is discussed with one of the Frontend Architecture Experts. + +This rule also applies if you plan to change the architecture of an existing feature. + +These decisions should be accessible to everyone, so please document it on the Merge Request. + +You can find the Frontend Architecture experts on the [team page][team-page]. + +You can find documentation about the desired architecture for a new feature built with Vue.js in [here][vue-section]. + ### Vue For more complex frontend features, we recommend using Vue.js. It shares @@ -238,8 +254,8 @@ readability. See the relevant style guides for our guidelines and for information on linting: - [SCSS][scss-style-guide] -- JavaScript - We defer to [AirBnb][airbnb-js-style-guide] on most style-related -conventions and enforce them with eslint. See [our current .eslintrc][eslistrc] +- JavaScript - We defer to [AirBnb][airbnb-js-style-guide] on most style-related +conventions and enforce them with eslint. See [our current .eslintrc][eslintrc] for specific rules and patterns. ## Testing @@ -439,3 +455,5 @@ Scenario: Developer can approve merge request [issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6 [airbnb-js-style-guide]: https://github.com/airbnb/javascript [eslintrc]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc +[team-page]: https://about.gitlab.com/team +[vue-section]: https://docs.gitlab.com/ce/development/frontend.html#how-to-build-a-new-feature-with-vue-js diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md index ead79ba6a10..794c8eb6bfe 100644 --- a/doc/development/ux_guide/copy.md +++ b/doc/development/ux_guide/copy.md @@ -167,6 +167,15 @@ A **comment** is a written piece of text that users of GitLab can create. Commen #### Discussion
A **discussion** is a group of 1 or more comments. A discussion can include subdiscussions. Some discussions have the special capability of being able to be **resolved**. Both the comments in the discussion and the discussion itself can be resolved.
+## Confirmation dialogs
+
+- Destruction buttons should be clear and always say what they are destroying.
+ E.g., `Delete page` instead of just `Delete`.
+- If the copy describes another action the user can take instead of the
+ destructive one, provide a way for them to do that as a secondary button.
+- Avoid the word `cancel` or `canceled` in the descriptive copy. It can be
+ confusing when you then see the `Cancel` button.
+
---
Portions of this page are modifications based on work created and shared by the [Android Open Source Project][android project] and used according to terms described in the [Creative Commons 2.5 Attribution License][creative commons].
diff --git a/doc/install/installation.md b/doc/install/installation.md index 8e74970b8e9..177e1a9378b 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -288,9 +288,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-17-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 9-0-stable gitlab -**Note:** You can change `8-17-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `9-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md index 4cc8be752c4..1fe38cf8d2a 100644 --- a/doc/update/8.17-to-9.0.md +++ b/doc/update/8.17-to-9.0.md @@ -1,3 +1,162 @@ +# From 8.17 to 9.0 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +We will continue supporting Ruby < 2.3 for the time being but we recommend you +upgrade to Ruby 2.3 if you're running a source installation, as this is the same +version that ships with our Omnibus package. + +You can check which version you are running with `ruby -v`. + +Download and compile Ruby: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz +echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz +cd ruby-2.3.3 +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Update Node + +GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and +it has a minimum requirement of node v4.3.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v4.3.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + +<https://nodejs.org/en/download/> + + +Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage +JavaScript dependencies. + +```bash +curl --location https://yarnpkg.com/install.sh | bash - +``` + +More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). + +### 5. Get latest code + +```bash +cd /home/git/gitlab + +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 9-0-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 9-0-stable-ee +``` + +### 6. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Install/update frontend asset dependencies +sudo -u git -H npm install --production + +# Clean up assets and cache +sudo -u git -H bundle exec rake gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production +``` + +**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). + +### 7. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production +``` + +### 8. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v5.0.0 +``` + +### 9. Update configuration files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +cd /home/git/gitlab + +git diff origin/8-17-stable:config/gitlab.yml.example origin/9-0-stable:config/gitlab.yml.example +``` + #### Configuration changes for repository storages This version introduces a new configuration structure for repository storages. @@ -85,3 +244,78 @@ via [/etc/default/gitlab]. [Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache [/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + +```bash +cd /home/git/gitlab + +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +For Ubuntu 16.04.1 LTS: + +```bash +sudo systemctl daemon-reload +``` + +### 10. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 11. Check application status + +Check if GitLab and its environment are configured correctly: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +To make sure you didn't miss anything run a more thorough check: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.17) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.16 to 8.17](8.16-to-8.17.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. + +[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/config/gitlab.yml.example diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index e7f7edd95c7..abd263c1dfc 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -116,7 +116,8 @@ module API finder_params = { project_id: user_project.id, - milestone_title: milestone.title + milestone_title: milestone.title, + sort: 'position_asc' } issues = IssuesFinder.new(current_user, finder_params).execute @@ -138,7 +139,8 @@ module API finder_params = { project_id: user_project.id, - milestone_id: milestone.id + milestone_id: milestone.id, + sort: 'position_asc' } merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index ecf6b6e068b..5476438b8fa 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -62,7 +62,7 @@ namespace :gitlab do ref = Shellwords.escape(args[:ref]) - migrations = `git diff #{ref}.. --name-only -- db/migrate`.lines + migrations = `git diff #{ref}.. --diff-filter=A --name-only -- db/migrate`.lines .map { |file| Rails.root.join(file.strip).to_s } .select { |file| File.file?(file) } diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb index 7df102067d6..a6c72b0b3ac 100644 --- a/spec/features/merge_requests/diff_notes_avatars_spec.rb +++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb @@ -23,6 +23,56 @@ feature 'Diff note avatars', feature: true, js: true do login_as user end + context 'discussion tab' do + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'does not show avatars on discussion tab' do + expect(page).not_to have_selector('.js-avatar-container') + expect(page).not_to have_selector('.diff-comment-avatar-holders') + end + + it 'does not render avatars after commening on discussion tab' do + click_button 'Reply...' + + page.within('.js-discussion-note-form') do + find('.note-textarea').native.send_keys('Test comment') + + click_button 'Comment' + end + + expect(page).to have_content('Test comment') + expect(page).not_to have_selector('.js-avatar-container') + expect(page).not_to have_selector('.diff-comment-avatar-holders') + end + end + + context 'commit view' do + before do + visit namespace_project_commit_path(project.namespace, project, merge_request.commits.first.id) + end + + it 'does not render avatar after commenting' do + first('.diff-line-num').trigger('mouseover') + find('.js-add-diff-note-button').click + + page.within('.js-discussion-note-form') do + find('.note-textarea').native.send_keys('test comment') + + click_button 'Comment' + + wait_for_ajax + end + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + + expect(page).to have_content('test comment') + expect(page).not_to have_selector('.js-avatar-container') + expect(page).not_to have_selector('.diff-comment-avatar-holders') + end + end + %w(inline parallel).each do |view| context "#{view} view" do before do diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb index a1643fd1f43..7c319af893b 100644 --- a/spec/features/projects/edit_spec.rb +++ b/spec/features/projects/edit_spec.rb @@ -21,36 +21,28 @@ feature 'Project edit', feature: true, js: true do expect(page).to have_selector('.merge-requests-feature', visible: false) end - it 'hides merge requests section after save' do - select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level') - - expect(page).to have_selector('.merge-requests-feature', visible: false) - - click_button 'Save changes' + context 'given project with merge_requests_disabled access level' do + let(:project) { create(:project, :merge_requests_disabled) } - wait_for_ajax - - expect(page).to have_selector('.merge-requests-feature', visible: false) + it 'hides merge requests section' do + expect(page).to have_selector('.merge-requests-feature', visible: false) + end end end context 'builds select' do - it 'hides merge requests section' do + it 'hides builds select section' do select('Disabled', from: 'project_project_feature_attributes_builds_access_level') expect(page).to have_selector('.builds-feature', visible: false) end - it 'hides merge requests section after save' do - select('Disabled', from: 'project_project_feature_attributes_builds_access_level') - - expect(page).to have_selector('.builds-feature', visible: false) + context 'given project with builds_disabled access level' do + let(:project) { create(:project, :builds_disabled) } - click_button 'Save changes' - - wait_for_ajax - - expect(page).to have_selector('.builds-feature', visible: false) + it 'hides builds select section' do + expect(page).to have_selector('.builds-feature', visible: false) + end end end end diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index d8517e4d3d1..dc0a62ade50 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,8 +1,9 @@ /* 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 */ -require('es6-promise').polyfill(); +import promisePolyfill from 'es6-promise'; +import AwardsHandler from '~/awards_handler'; -const AwardsHandler = require('~/awards_handler'); +promisePolyfill.polyfill(); (function() { var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu; diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/gl_emoji_spec.js index e94e220b19f..7ab0b37f2ec 100644 --- a/spec/javascripts/gl_emoji_spec.js +++ b/spec/javascripts/gl_emoji_spec.js @@ -1,16 +1,15 @@ +import '~/extensions/string'; +import '~/extensions/array'; -require('~/extensions/string'); -require('~/extensions/array'); - -const glEmoji = require('~/behaviors/gl_emoji'); - -const glEmojiTag = glEmoji.glEmojiTag; -const isEmojiUnicodeSupported = glEmoji.isEmojiUnicodeSupported; -const isFlagEmoji = glEmoji.isFlagEmoji; -const isKeycapEmoji = glEmoji.isKeycapEmoji; -const isSkinToneComboEmoji = glEmoji.isSkinToneComboEmoji; -const isHorceRacingSkinToneComboEmoji = glEmoji.isHorceRacingSkinToneComboEmoji; -const isPersonZwjEmoji = glEmoji.isPersonZwjEmoji; +import { glEmojiTag } from '~/behaviors/gl_emoji'; +import { + isEmojiUnicodeSupported, + isFlagEmoji, + isKeycapEmoji, + isSkinToneComboEmoji, + isHorceRacingSkinToneComboEmoji, + isPersonZwjEmoji, +} from '~/behaviors/gl_emoji/is_emoji_unicode_supported'; const emptySupportMap = { personZwj: false, diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js index 7cdade01e00..e504d41d4d4 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js @@ -1,7 +1,7 @@ /* eslint-disable no-new */ -require('~/flash'); -require('~/mini_pipeline_graph_dropdown'); +import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; +import '~/flash'; (() => { describe('Mini Pipeline Graph Dropdown', () => { @@ -13,7 +13,7 @@ require('~/mini_pipeline_graph_dropdown'); describe('When is initialized', () => { it('should initialize without errors when no options are given', () => { - const miniPipelineGraph = new window.gl.MiniPipelineGraph(); + const miniPipelineGraph = new MiniPipelineGraph(); expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container'); }); @@ -21,7 +21,7 @@ require('~/mini_pipeline_graph_dropdown'); it('should set the container as the given prop', () => { const container = '.foo'; - const miniPipelineGraph = new window.gl.MiniPipelineGraph({ container }); + const miniPipelineGraph = new MiniPipelineGraph({ container }); expect(miniPipelineGraph.container).toEqual(container); }); @@ -29,9 +29,9 @@ require('~/mini_pipeline_graph_dropdown'); describe('When dropdown is clicked', () => { it('should call getBuildsList', () => { - const getBuildsListSpy = spyOn(gl.MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {}); + const getBuildsListSpy = spyOn(MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {}); - new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); + new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); document.querySelector('.js-builds-dropdown-button').click(); @@ -41,11 +41,32 @@ require('~/mini_pipeline_graph_dropdown'); it('should make a request to the endpoint provided in the html', () => { const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {}); - new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); + new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); document.querySelector('.js-builds-dropdown-button').click(); expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar'); }); + + it('should not close when user uses cmd/ctrl + click', () => { + spyOn($, 'ajax').and.callFake(function (params) { + params.success({ + html: `<li> + <a class="mini-pipeline-graph-dropdown-item" href="#"> + <span class="ci-status-icon ci-status-icon-failed"></span> + <span class="ci-build-text">build</span> + </a> + <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a> + </li>`, + }); + }); + new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); + + document.querySelector('.js-builds-dropdown-button').click(); + + document.querySelector('a.mini-pipeline-graph-dropdown-item').click(); + + expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true); + }); }); }); })(); diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js new file mode 100644 index 00000000000..d966226909b --- /dev/null +++ b/spec/javascripts/pager_spec.js @@ -0,0 +1,90 @@ +/* global fixture */ + +require('~/pager'); + +describe('pager', () => { + const Pager = window.Pager; + + it('is defined on window', () => { + expect(window.Pager).toBeDefined(); + }); + + describe('init', () => { + const originalHref = window.location.href; + + beforeEach(() => { + setFixtures('<div class="content_list"></div><div class="loading"></div>'); + spyOn($, 'ajax'); + }); + + afterEach(() => { + window.history.replaceState({}, null, originalHref); + }); + + it('should use data-href attribute from list element', () => { + const href = `${gl.TEST_HOST}/some_list.json`; + setFixtures(`<div class="content_list" data-href="${href}"></div>`); + Pager.init(); + expect(Pager.url).toBe(href); + }); + + it('should use current url if data-href attribute not provided', () => { + const href = `${gl.TEST_HOST}/some_list`; + spyOn(gl.utils, 'removeParams').and.returnValue(href); + Pager.init(); + expect(Pager.url).toBe(href); + }); + + it('should get initial offset from query parameter', () => { + window.history.replaceState({}, null, '?offset=100'); + Pager.init(); + expect(Pager.offset).toBe(100); + }); + + it('keeps extra query parameters from url', () => { + window.history.replaceState({}, null, '?filter=test&offset=100'); + const href = `${gl.TEST_HOST}/some_list?filter=test`; + spyOn(gl.utils, 'removeParams').and.returnValue(href); + Pager.init(); + expect(gl.utils.removeParams).toHaveBeenCalledWith(['limit', 'offset']); + expect(Pager.url).toEqual(href); + }); + }); + + describe('getOld', () => { + beforeEach(() => { + setFixtures('<div class="content_list" data-href="/some_list"></div><div class="loading"></div>'); + Pager.init(); + }); + + it('shows loader while loading next page', () => { + spyOn(Pager.loading, 'show'); + Pager.getOld(); + expect(Pager.loading.show).toHaveBeenCalled(); + }); + + it('hides loader on success', () => { + spyOn($, 'ajax').and.callFake(options => options.success({})); + spyOn(Pager.loading, 'hide'); + Pager.getOld(); + expect(Pager.loading.hide).toHaveBeenCalled(); + }); + + it('hides loader on error', () => { + spyOn($, 'ajax').and.callFake(options => options.error()); + spyOn(Pager.loading, 'hide'); + Pager.getOld(); + expect(Pager.loading.hide).toHaveBeenCalled(); + }); + + it('sends request to url with offset and limit params', () => { + spyOn($, 'ajax'); + Pager.offset = 100; + Pager.limit = 20; + Pager.getOld(); + const [{ data, url }] = $.ajax.calls.argsFor(0); + expect(data).toBe('limit=20&offset=100'); + expect(url).toBe('/some_list'); + }); + }); +}); diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 3ea62df62f2..9962c987110 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -197,6 +197,24 @@ describe Ci::Pipeline, models: true do end end end + + context 'when there is a stage with warnings' do + before do + create(:commit_status, pipeline: pipeline, + stage: 'deploy', + name: 'prod:2', + stage_idx: 2, + status: 'failed', + allow_failure: true) + end + + it 'populates stage with correct number of warnings' do + deploy_stage = pipeline.stages.third + + expect(deploy_stage).not_to receive(:statuses) + expect(deploy_stage).to have_warnings + end + end end describe '#stages_count' do diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index c4a9743a4e2..c38faf32f7d 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -170,22 +170,31 @@ describe Ci::Stage, models: true do context 'when stage has warnings' do context 'when using memoized warnings flag' do context 'when there are warnings' do - let(:stage) { build(:ci_stage, warnings: true) } + let(:stage) { build(:ci_stage, warnings: 2) } - it 'has memoized warnings' do + it 'returns true using memoized value' do expect(stage).not_to receive(:statuses) expect(stage).to have_warnings end end context 'when there are no warnings' do - let(:stage) { build(:ci_stage, warnings: false) } + let(:stage) { build(:ci_stage, warnings: 0) } - it 'has memoized warnings' do + it 'returns false using memoized value' do expect(stage).not_to receive(:statuses) expect(stage).not_to have_warnings end end + + context 'when number of warnings is not a valid value' do + let(:stage) { build(:ci_stage, warnings: true) } + + it 'calculates statuses using database queries' do + expect(stage).to receive(:statuses).and_call_original + expect(stage).not_to have_warnings + end + end end context 'when calculating warnings from statuses' do diff --git a/spec/models/project_services/issue_tracker_service_spec.rb b/spec/models/project_services/issue_tracker_service_spec.rb new file mode 100644 index 00000000000..fbe6f344a98 --- /dev/null +++ b/spec/models/project_services/issue_tracker_service_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe IssueTrackerService, models: true do + describe 'Validations' do + let(:project) { create :project } + + describe 'only one issue tracker per project' do + let(:service) { RedmineService.new(project: project, active: true) } + + before do + create(:service, project: project, active: true, category: 'issue_tracker') + end + + context 'when service is changed manually by user' do + it 'executes the validation' do + valid = service.valid?(:manual_change) + + expect(valid).to be_falsey + expect(service.errors[:base]).to include( + 'Another issue tracker is already in use. Only one issue tracker service can be active at a time' + ) + end + end + + context 'when service is changed internally' do + it 'does not execute the validation' do + expect(service.valid?).to be_truthy + end + end + end + end +end diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index 3bb8b6fdbeb..7fb728fed6f 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -243,8 +243,8 @@ describe API::Milestones, api: true do describe 'confidential issues' do let(:public_project) { create(:empty_project, :public) } let(:milestone) { create(:milestone, project: public_project) } - let(:issue) { create(:issue, project: public_project) } - let(:confidential_issue) { create(:issue, confidential: true, project: public_project) } + let(:issue) { create(:issue, project: public_project, position: 2) } + let(:confidential_issue) { create(:issue, confidential: true, project: public_project, position: 1) } before do public_project.team << [user, :developer] @@ -283,11 +283,24 @@ describe API::Milestones, api: true do expect(json_response.size).to eq(1) expect(json_response.map { |issue| issue['id'] }).to include(issue.id) end + + it 'returns issues ordered by position asc' do + get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(2) + expect(json_response.first['id']).to eq(confidential_issue.id) + expect(json_response.second['id']).to eq(issue.id) + end end end describe 'GET /projects/:id/milestones/:milestone_id/merge_requests' do - let(:merge_request) { create(:merge_request, source_project: project) } + let(:merge_request) { create(:merge_request, source_project: project, position: 2) } + let(:another_merge_request) { create(:merge_request, :simple, source_project: project, position: 1) } + before do milestone.merge_requests << merge_request end @@ -320,5 +333,18 @@ describe API::Milestones, api: true do expect(response).to have_http_status(401) end + + it 'returns merge_requests ordered by position asc' do + milestone.merge_requests << another_merge_request + + get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(2) + expect(json_response.first['id']).to eq(another_merge_request.id) + expect(json_response.second['id']).to eq(merge_request.id) + end end end |