Commit 8431b2b5 authored by blackst0ne's avatar blackst0ne

Merge remote-tracking branch 'upstream/master' into fix_visibility_level

parents 245020a1 9ed3db91
...@@ -94,6 +94,10 @@ look for [issues with the label `Accepting Merge Requests` and weight < 5][accep ...@@ -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 These issues will be of reasonable size and challenge, for anyone to start
contributing to GitLab. contributing to GitLab.
## Workflow labels
Labelling issues is described in the [GitLab Inc engineering workflow].
## Implement design & UI elements ## Implement design & UI elements
Please see the [UX Guide for GitLab]. Please see the [UX Guide for GitLab].
...@@ -299,10 +303,13 @@ request is as follows: ...@@ -299,10 +303,13 @@ request is as follows:
1. [Generate a changelog entry with `bin/changelog`][changelog] 1. [Generate a changelog entry with `bin/changelog`][changelog]
1. If you are writing documentation, make sure to follow the 1. If you are writing documentation, make sure to follow the
[documentation styleguide][doc-styleguide] [documentation styleguide][doc-styleguide]
1. If you have multiple commits please combine them into one commit by 1. If you have multiple commits please combine them into a few logically
[squashing them][git-squash] organized commits by [squashing them][git-squash]
1. Push the commit(s) to your fork 1. Push the commit(s) to your fork
1. Submit a merge request (MR) to the `master` branch 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 title should describe the change you want to make
1. The MR description should give a motive for your change and the method you 1. The MR description should give a motive for your change and the method you
used to achieve it. used to achieve it.
...@@ -345,13 +352,31 @@ The ['How to get faster PR reviews' document of Kubernetes](https://github.com/k ...@@ -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 For examples of feedback on merge requests please look at already
[closed merge requests][closed-merge-requests]. If you would like quick feedback [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 on your merge request feel free to mention someone from the [core team] or one
[core team] or one of the [Merge request coaches](https://about.gitlab.com/team/). of the [Merge request coaches][team].
Please ensure that your merge request meets the contribution acceptance criteria. Please ensure that your merge request meets the contribution acceptance criteria.
When having your code reviewed and when reviewing merge requests please take the When having your code reviewed and when reviewing merge requests please take the
[code review guidelines](doc/development/code_review.md) into account. [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 ### Contribution acceptance criteria
1. The change is as small as possible 1. The change is as small as possible
...@@ -489,6 +514,7 @@ This Code of Conduct is adapted from the [Contributor Covenant][contributor-cove ...@@ -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/). 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/ [core team]: https://about.gitlab.com/core-team/
[team]: https://about.gitlab.com/team/
[getting-help]: https://about.gitlab.com/getting-help/ [getting-help]: https://about.gitlab.com/getting-help/
[codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq [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 [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 ...@@ -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" [newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
[UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/ [UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/
[license-finder-doc]: doc/development/licensing.md [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.
...@@ -2,7 +2,7 @@ GEM ...@@ -2,7 +2,7 @@ GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
RedCloth (4.3.2) RedCloth (4.3.2)
ace-rails-ap (4.1.0) ace-rails-ap (4.1.2)
actionmailer (4.2.8) actionmailer (4.2.8)
actionpack (= 4.2.8) actionpack (= 4.2.8)
actionview (= 4.2.8) actionview (= 4.2.8)
......
/* global Cookies */ /* global Cookies */
const emojiMap = require('emoji-map'); import emojiMap from 'emojis/digests.json';
const emojiAliases = require('emoji-aliases'); import emojiAliases from 'emojis/aliases.json';
const glEmoji = require('./behaviors/gl_emoji'); import { glEmojiTag } from './behaviors/gl_emoji';
const glEmojiTag = glEmoji.glEmojiTag;
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const requestAnimationFrame = window.requestAnimationFrame || const requestAnimationFrame = window.requestAnimationFrame ||
...@@ -47,12 +45,12 @@ function buildCategoryMap() { ...@@ -47,12 +45,12 @@ function buildCategoryMap() {
}); });
} }
function renderCategory(name, emojiList) { function renderCategory(name, emojiList, opts = {}) {
return ` return `
<h5 class="emoji-menu-title"> <h5 class="emoji-menu-title">
${name} ${name}
</h5> </h5>
<ul class="clearfix emoji-menu-list"> <ul class="clearfix emoji-menu-list ${opts.menuListClass}">
${emojiList.map(emojiName => ` ${emojiList.map(emojiName => `
<li class="emoji-menu-list-item"> <li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button"> <button class="emoji-menu-btn text-center js-emoji-btn" type="button">
...@@ -142,9 +140,6 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) { ...@@ -142,9 +140,6 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) {
const $createdMenu = $('.emoji-menu'); const $createdMenu = $('.emoji-menu');
$addBtn.removeClass('is-loading'); $addBtn.removeClass('is-loading');
this.positionMenu($createdMenu, $addBtn); this.positionMenu($createdMenu, $addBtn);
if (!this.frequentEmojiBlockRendered) {
this.renderFrequentlyUsedBlock();
}
return setTimeout(() => { return setTimeout(() => {
$createdMenu.addClass('is-visible'); $createdMenu.addClass('is-visible');
$('#emoji_search').focus(); $('#emoji_search').focus();
...@@ -167,11 +162,21 @@ AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) { ...@@ -167,11 +162,21 @@ AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) {
const emojisInCategory = categoryMap[categoryNameKey]; const emojisInCategory = categoryMap[categoryNameKey];
const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
// Render the frequently used
const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
let frequentlyUsedCatgegory = '';
if (frequentlyUsedEmojis.length > 0) {
frequentlyUsedCatgegory = renderCategory('Frequently used', frequentlyUsedEmojis, {
menuListClass: 'frequent-emojis',
});
}
const emojiMenuMarkup = ` const emojiMenuMarkup = `
<div class="emoji-menu"> <div class="emoji-menu">
<input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" placeholder="Search emoji" /> <input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" placeholder="Search emoji" />
<div class="emoji-menu-content"> <div class="emoji-menu-content">
${frequentlyUsedCatgegory}
${firstCategory} ${firstCategory}
</div> </div>
</div> </div>
...@@ -459,19 +464,6 @@ AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmoj ...@@ -459,19 +464,6 @@ AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmoj
return _.compact(_.uniq(frequentlyUsedEmojis)); return _.compact(_.uniq(frequentlyUsedEmojis));
}; };
AwardsHandler.prototype.renderFrequentlyUsedBlock = function renderFrequentlyUsedBlock() {
if (Cookies.get('frequently_used_emojis')) {
const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
const ul = $('<ul class="clearfix emoji-menu-list frequent-emojis">');
for (let i = 0, len = frequentlyUsedEmojis.length; i < len; i += 1) {
const emoji = frequentlyUsedEmojis[i];
$(`.emoji-menu-content [data-name="${emoji}"]`).closest('li').clone().appendTo(ul);
}
$('.emoji-menu-content').prepend(ul).prepend($('<h5>').text('Frequently used'));
}
this.frequentEmojiBlockRendered = true;
};
AwardsHandler.prototype.setupSearch = function setupSearch() { AwardsHandler.prototype.setupSearch = function setupSearch() {
this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => { this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => {
const term = $(e.target).val().trim(); const term = $(e.target).val().trim();
...@@ -515,4 +507,4 @@ AwardsHandler.prototype.destroy = function destroy() { ...@@ -515,4 +507,4 @@ AwardsHandler.prototype.destroy = function destroy() {
$('.emoji-menu').remove(); $('.emoji-menu').remove();
}; };
module.exports = AwardsHandler; export default AwardsHandler;
const installCustomElements = require('document-register-element'); import installCustomElements from 'document-register-element';
const emojiMap = require('emoji-map'); import emojiMap from 'emojis/digests.json';
const emojiAliases = require('emoji-aliases'); import emojiAliases from 'emojis/aliases.json';
const generatedUnicodeSupportMap = require('./gl_emoji/unicode_support_map'); import { getUnicodeSupportMap } from './gl_emoji/unicode_support_map';
const spreadString = require('./gl_emoji/spread_string'); import { isEmojiUnicodeSupported } from './gl_emoji/is_emoji_unicode_supported';
installCustomElements(window); installCustomElements(window);
const generatedUnicodeSupportMap = getUnicodeSupportMap();
function emojiImageTag(name, src) { function emojiImageTag(name, src) {
return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`; return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`;
} }
...@@ -55,163 +57,49 @@ function glEmojiTag(inputName, options) { ...@@ -55,163 +57,49 @@ function glEmojiTag(inputName, options) {
`; `;
} }
// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/ function installGlEmojiElement() {
const flagACodePoint = 127462; // parseInt('1F1E6', 16) const GlEmojiElementProto = Object.create(HTMLElement.prototype);
const flagZCodePoint = 127487; // parseInt('1F1FF', 16) GlEmojiElementProto.createdCallback = function createdCallback() {
function isFlagEmoji(emojiUnicode) { const emojiUnicode = this.textContent.trim();
const cp = emojiUnicode.codePointAt(0); const {
// Length 4 because flags are made of 2 characters which are surrogate pairs name,
return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint; unicodeVersion,
} fallbackSrc,
fallbackSpriteClass,
// Chrome <57 renders keycaps oddly } = this.dataset;
// 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 const isEmojiUnicode = this.childNodes && Array.prototype.every.call(
function isKeycapEmoji(emojiUnicode) { this.childNodes,
return emojiUnicode.length === 3 && emojiUnicode[2] === '\u20E3'; childNode => childNode.nodeType === 3,
} );
const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
// Check for a skin tone variation emoji which aren't always supported const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
const tone1 = 127995;// parseInt('1F3FB', 16)
const tone5 = 127999;// parseInt('1F3FF', 16) if (
function isSkinToneComboEmoji(emojiUnicode) { isEmojiUnicode &&
return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => { !isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion)
const cp = char.codePointAt(0); ) {
return cp >= tone1 && cp <= tone5; // CSS sprite fallback takes precedence over image fallback
}); if (hasCssSpriteFalback) {
} // IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon');
// macOS supports most skin tone emoji's but this.classList.add(fallbackSpriteClass);
// doesn't support the skin tone versions of horse racing } else if (hasImageFallback) {
const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16) this.innerHTML = emojiImageTag(name, fallbackSrc);
function isHorceRacingSkinToneComboEmoji(emojiUnicode) { } else {
return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint && const src = assembleFallbackImageSrc(name);
isSkinToneComboEmoji(emojiUnicode); this.innerHTML = emojiImageTag(name, src);
} }
// 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 document.registerElement('gl-emoji', {
return unicodeSupportMap[unicodeVersion] && prototype: GlEmojiElementProto,
!(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) && });
checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) &&
checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) &&
checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) &&
checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode);
} }
const GlEmojiElementProto = Object.create(HTMLElement.prototype); export {
GlEmojiElementProto.createdCallback = function createdCallback() { installGlEmojiElement,
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,
glEmojiTag, glEmojiTag,
isEmojiUnicodeSupported, emojiImageTag,
isFlagEmoji,
isKeycapEmoji,
isSkinToneComboEmoji,
isHorceRacingSkinToneComboEmoji,
isPersonZwjEmoji,
}; };
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,
};
...@@ -47,4 +47,4 @@ function spreadString(str) { ...@@ -47,4 +47,4 @@ function spreadString(str) {
return arr; return arr;
} }
module.exports = spreadString; export default spreadString;
...@@ -68,7 +68,7 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche ...@@ -68,7 +68,7 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche
// See 32px, https://i.imgur.com/htY6Zym.png // See 32px, https://i.imgur.com/htY6Zym.png
// See 16px, https://i.imgur.com/FPPsIF8.png // See 16px, https://i.imgur.com/FPPsIF8.png
const fontSize = 16; const fontSize = 16;
function testUnicodeSupportMap(testMap) { function generateUnicodeSupportMap(testMap) {
const testMapKeys = Object.keys(testMap); const testMapKeys = Object.keys(testMap);
const numTestEntries = testMapKeys const numTestEntries = testMapKeys
.reduce((list, testKey) => list.concat(testMap[testKey]), []).length; .reduce((list, testKey) => list.concat(testMap[testKey]), []).length;
...@@ -138,17 +138,24 @@ function testUnicodeSupportMap(testMap) { ...@@ -138,17 +138,24 @@ function testUnicodeSupportMap(testMap) {
return resultMap; return resultMap;
} }
let unicodeSupportMap; function getUnicodeSupportMap() {
const userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent'); let unicodeSupportMap;
try { const userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map')); try {
} catch (err) { unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
// swallow } catch (err) {
} // swallow
if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) { }
unicodeSupportMap = testUnicodeSupportMap(unicodeSupportTestMap); if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent); unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap)); 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,
};
...@@ -39,6 +39,7 @@ import Issue from './issue'; ...@@ -39,6 +39,7 @@ import Issue from './issue';
import BindInOut from './behaviors/bind_in_out'; import BindInOut from './behaviors/bind_in_out';
import GroupsList from './groups_list'; import GroupsList from './groups_list';
import ProjectsList from './projects_list'; import ProjectsList from './projects_list';
import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
const ShortcutsBlob = require('./shortcuts_blob'); const ShortcutsBlob = require('./shortcuts_blob');
const UserCallout = require('./user_callout'); const UserCallout = require('./user_callout');
...@@ -181,7 +182,7 @@ const UserCallout = require('./user_callout'); ...@@ -181,7 +182,7 @@ const UserCallout = require('./user_callout');
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
break; break;
case 'projects:commit:pipelines': case 'projects:commit:pipelines':
new gl.MiniPipelineGraph({ new MiniPipelineGraph({
container: '.js-pipeline-table', container: '.js-pipeline-table',
}).bindEvents(); }).bindEvents();
break; break;
......
require('string.prototype.codepointat'); import 'string.prototype.codepointat';
require('string.fromcodepoint'); import 'string.fromcodepoint';
/* 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 */ /* 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'); import emojiMap from 'emojis/digests.json';
const emojiAliases = require('emoji-aliases'); import emojiAliases from 'emojis/aliases.json';
const glEmoji = require('./behaviors/gl_emoji'); import { glEmojiTag } from '~/behaviors/gl_emoji';
const glEmojiTag = glEmoji.glEmojiTag;
// Creates the variables for setting up GFM auto-completion // Creates the variables for setting up GFM auto-completion
(function() { (function() {
......
...@@ -66,6 +66,13 @@ ...@@ -66,6 +66,13 @@
return results; return results;
})()).join('&'); })()).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) { w.gl.utils.getLocationHash = function(url) {
var hashIndex; var hashIndex;
if (typeof url === 'undefined') { if (typeof url === 'undefined') {
......
This diff is collapsed.
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
/* global notifyPermissions */ /* global notifyPermissions */
/* global merge_request_widget */ /* global merge_request_widget */
require('./smart_interval'); import './smart_interval';
import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
((global) => { ((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; }; 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'); ...@@ -285,7 +286,7 @@ require('./smart_interval');
}; };
MergeRequestWidget.prototype.initMiniPipelineGraph = function() { MergeRequestWidget.prototype.initMiniPipelineGraph = function() {
new gl.MiniPipelineGraph({ new MiniPipelineGraph({
container: '.js-pipeline-inline-mr-widget-graph:visible', container: '.js-pipeline-inline-mr-widget-graph:visible',
}).bindEvents(); }).bindEvents();
}; };
......
...@@ -15,81 +15,96 @@ ...@@ -15,81 +15,96 @@
* <div class="js-builds-dropdown-container dropdown-menu"></div> * <div class="js-builds-dropdown-container dropdown-menu"></div>
* </div> * </div>
*/ */
(() => {
class MiniPipelineGraph {
constructor(opts = {}) {
this.container = opts.container || '';
this.dropdownListSelector = '.js-builds-dropdown-container';
this.getBuildsList = this.getBuildsList.bind(this);
}
/** export default class MiniPipelineGraph {
* Adds the event listener when the dropdown is opened. constructor(opts = {}) {
* All dropdown events are fired at the .dropdown-menu's parent element. this.container = opts.container || '';
*/ this.dropdownListSelector = '.js-builds-dropdown-container';
bindEvents() { this.getBuildsList = this.getBuildsList.bind(this);
$(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList); }
}
/**
* 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. * 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,
* @param {HTMLElement} stageContainer * so we stop propagation of the click event inside the dropdown.
* @param {Object} data *
*/ * Since this component is rendered multiple times per page we need to guarantee we only
renderBuildsList(stageContainer, data) { * target the click event of this component.
const dropdownContainer = stageContainer.parentElement.querySelector( */
`${this.dropdownListSelector} .js-builds-dropdown-list`, 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`,
);
/** dropdownContainer.innerHTML = data;
* 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;
return $.ajax({ /**
dataType: 'json', * For the clicked stage, gets the list of builds.
type: 'GET', *
url: endpoint, * All dropdown events have a relatedTarget property,
beforeSend: () => { * whose value is the toggling anchor element.
this.renderBuildsList(button, ''); *
this.toggleLoading(button); * @param {Object} e bootstrap dropdown event
}, * @return {Promise}
success: (data) => { */
this.toggleLoading(button); getBuildsList(e) {
this.renderBuildsList(button, data.html); const button = e.relatedTarget;
}, const endpoint = button.dataset.stageEndpoint;
error: () => {
this.toggleLoading(button);
new Flash('An error occurred while fetching the builds.', 'alert');
},
});
}
/** return $.ajax({
* Toggles the visibility of the loading icon. dataType: 'json',
* type: 'GET',
* @param {HTMLElement} stageContainer url: endpoint,
* @return {type} beforeSend: () => {
*/ this.renderBuildsList(button, '');
toggleLoading(stageContainer) { this.toggleLoading(button);
stageContainer.parentElement.querySelector( },
`${this.dropdownListSelector} .js-builds-dropdown-loading`, success: (data) => {
).classList.toggle('hidden'); 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');
}
}
/* 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 */ /* 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 Flash */
/* global Autosave */ /* global Autosave */
/* global Cookies */
/* global ResolveService */ /* global ResolveService */
/* global mrRefreshWidgetUrl */ /* global mrRefreshWidgetUrl */
require('./autosave'); require('./autosave');
window.autosize = require('vendor/autosize'); window.autosize = require('vendor/autosize');
window.Dropzone = require('dropzone'); window.Dropzone = require('dropzone');
window.Cookies = require('js-cookie');
require('./dropzone_input'); require('./dropzone_input');
require('./gfm_auto_complete'); require('./gfm_auto_complete');
require('vendor/jquery.caret'); // required by jquery.atwho require('vendor/jquery.caret'); // required by jquery.atwho
...@@ -42,7 +44,6 @@ require('./task_list'); ...@@ -42,7 +44,6 @@ require('./task_list');
this.notes_url = notes_url; this.notes_url = notes_url;
this.note_ids = note_ids; this.note_ids = note_ids;
this.last_fetched_at = last_fetched_at; this.last_fetched_at = last_fetched_at;
this.view = view;
this.noteable_url = document.URL; this.noteable_url = document.URL;
this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge")); this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge"));
this.basePollingInterval = 15000; this.basePollingInterval = 15000;
...@@ -57,6 +58,7 @@ require('./task_list'); ...@@ -57,6 +58,7 @@ require('./task_list');
selector: '.notes' selector: '.notes'
}); });
this.collapseLongCommitList(); this.collapseLongCommitList();
this.setViewType(view);
// We are in the Merge Requests page so we need another edit form for Changes tab // We are in the Merge Requests page so we need another edit form for Changes tab
if (gl.utils.getPagePath(1) === 'merge_requests') { if (gl.utils.getPagePath(1) === 'merge_requests') {
...@@ -65,6 +67,10 @@ require('./task_list'); ...@@ -65,6 +67,10 @@ require('./task_list');
} }
} }
Notes.prototype.setViewType = function(view) {
this.view = Cookies.get('diff_view') || view;
};
Notes.prototype.addBinding = function() { Notes.prototype.addBinding = function() {
// add note to UI after creation // add note to UI after creation
$(document).on("ajax:success", ".js-main-target-form", this.addNote); $(document).on("ajax:success", ".js-main-target-form", this.addNote);
...@@ -302,7 +308,7 @@ require('./task_list'); ...@@ -302,7 +308,7 @@ require('./task_list');
}; };
Notes.prototype.isParallelView = function() { Notes.prototype.isParallelView = function() {
return this.view === 'parallel'; return Cookies.get('diff_view') === 'parallel';
}; };
/* /*
......
require('~/lib/utils/common_utils');
require('~/lib/utils/url_utility');
(() => { (() => {
const ENDLESS_SCROLL_BOTTOM_PX = 400; const ENDLESS_SCROLL_BOTTOM_PX = 400;
const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000; const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
const Pager = { const Pager = {
init(limit = 0, preload = false, disable = false, callback = $.noop) { init(limit = 0, preload = false, disable = false, callback = $.noop) {
this.url = $('.content_list').data('href') || gl.utils.removeParams(['limit', 'offset']);
this.limit = limit; this.limit = limit;
this.offset = this.limit; this.offset = parseInt(gl.utils.getParameterByName('offset'), 10) || this.limit;
this.disable = disable; this.disable = disable;
this.callback = callback; this.callback = callback;
this.loading = $('.loading').first(); this.loading = $('.loading').first();
...@@ -20,7 +24,7 @@ ...@@ -20,7 +24,7 @@
this.loading.show(); this.loading.show();
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: $('.content_list').data('href') || window.location.href, url: this.url,
data: `limit=${this.limit}&offset=${this.offset}`, data: `limit=${this.limit}&offset=${this.offset}`,
dataType: 'json', dataType: 'json',
error: () => this.loading.hide(), error: () => this.loading.hide(),
......
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
white-space: nowrap; white-space: nowrap;
&[disabled] { &[disabled] {
background-color: $input-bg-disabled; opacity: .65;
cursor: not-allowed; cursor: not-allowed;
} }
......
...@@ -24,10 +24,6 @@ ...@@ -24,10 +24,6 @@
color: inherit; color: inherit;
} }
.btn-success.dropdown-toggle:disabled {
background-color: $gl-success;
}
.accept-merge-request { .accept-merge-request {
&.ci-pending, &.ci-pending,
&.ci-running { &.ci-running {
......
...@@ -18,8 +18,7 @@ class AutocompleteController < ApplicationController ...@@ -18,8 +18,7 @@ class AutocompleteController < ApplicationController
if params[:search].blank? if params[:search].blank?
# Include current user if available to filter by "Me" # Include current user if available to filter by "Me"
if params[:current_user].present? && current_user if params[:current_user].present? && current_user
@users = @users.where.not(id: current_user.id) @users = [current_user, *@users].uniq
@users = [current_user, *@users]
end end
if params[:author_id].present? if params[:author_id].present?
......
...@@ -13,7 +13,8 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -13,7 +13,8 @@ class Projects::ServicesController < Projects::ApplicationController
end end
def update def update
if @service.update_attributes(service_params[:service]) @service.assign_attributes(service_params[:service])
if @service.save(context: :manual_change)
redirect_to( redirect_to(
edit_namespace_project_service_path(@project.namespace, @project, @service.to_param), edit_namespace_project_service_path(@project.namespace, @project, @service.to_param),
notice: 'Successfully updated.' notice: 'Successfully updated.'
......
...@@ -7,47 +7,18 @@ module Projects ...@@ -7,47 +7,18 @@ module Projects
@sort = params[:sort].presence || sort_value_name @sort = params[:sort].presence || sort_value_name
@group_links = @project.project_group_links @group_links = @project.project_group_links
@project_members = @project.project_members
@project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
group = @project.group
# group links
@group_links = @project.project_group_links.all
@skip_groups = @group_links.pluck(:group_id) @skip_groups = @group_links.pluck(:group_id)
@skip_groups << @project.namespace_id unless @project.personal? @skip_groups << @project.namespace_id unless @project.personal?
if group @project_members = MembersFinder.new(@project, current_user).execute
# We need `.where.not(user_id: nil)` here otherwise when a group has an
# invitee, it would make the following query return 0 rows since a NULL
# user_id would be present in the subquery
# See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
group_members = MembersFinder.new(@project_members, group).execute(current_user)
end
if params[:search].present? if params[:search].present?
user_ids = @project.users.search(params[:search]).select(:id) @project_members = @project_members.joins(:user).merge(User.search(params[:search]))
@project_members = @project_members.where(user_id: user_ids) @group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
if group_members
user_ids = group.users.search(params[:search]).select(:id)
group_members = group_members.where(user_id: user_ids)
end
@group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end end
wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"] @project_members = @project_members.sort(@sort).page(params[:page])
wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
@project_members = Member.
where(wheres.join(' OR ')).
sort(@sort).
page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user) @requesters = AccessRequestsFinder.new(@project).execute(current_user)
@project_member = @project.project_members.new @project_member = @project.project_members.new
end end
end end
......
class GroupMembersFinder < Projects::ApplicationController class GroupMembersFinder
def initialize(group) def initialize(group)
@group = group @group = group
end end
......
class MembersFinder < Projects::ApplicationController class MembersFinder
def initialize(project_members, project_group) attr_reader :project, :current_user, :group
@project_members = project_members
@project_group = project_group def initialize(project, current_user)
@project = project
@current_user = current_user
@group = project.group
end
def execute
project_members = project.project_members
project_members = project_members.non_invite unless can?(current_user, :admin_project, project)
wheres = ["members.id IN (#{project_members.select(:id).to_sql})"]
if group
# We need `.where.not(user_id: nil)` here otherwise when a group has an
# invitee, it would make the following query return 0 rows since a NULL
# user_id would be present in the subquery
# See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
non_null_user_ids = project_members.where.not(user_id: nil).select(:user_id)
group_members = GroupMembersFinder.new(group).execute
group_members = group_members.where.not(user_id: non_null_user_ids)
group_members = group_members.non_invite unless can?(current_user, :admin_group, group)
wheres << "members.id IN (#{group_members.select(:id).to_sql})"
end
Member.where(wheres.join(' OR '))
end end
def execute(current_user) def can?(*args)
non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id) Ability.allowed?(*args)
group_members = @project_group.group_members.where.not(user_id: non_null_user_ids)
group_members = group_members.non_invite unless can?(current_user, :admin_group, @project_group)
group_members
end end
end end
...@@ -15,6 +15,8 @@ module CiStatusHelper ...@@ -15,6 +15,8 @@ module CiStatusHelper
'passed' 'passed'
when 'success_with_warnings' when 'success_with_warnings'
'passed with warnings' 'passed with warnings'
when 'manual'
'waiting for manual action'
else else
status status
end end
......
...@@ -36,7 +36,7 @@ module PreferencesHelper ...@@ -36,7 +36,7 @@ module PreferencesHelper
def project_view_choices def project_view_choices
[ [
['Files and Readme (default)', :files], ['Files and Readme (default)', :files],
['Activity view', :activity] ['Activity', :activity]
] ]
end end
......
...@@ -144,7 +144,7 @@ module Ci ...@@ -144,7 +144,7 @@ module Ci
status_sql = statuses.latest.where('stage=sg.stage').status_sql 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 .where('stage=sg.stage').failed_but_allowed.to_sql
stages_with_statuses = CommitStatus.from(stages_query, :sg) stages_with_statuses = CommitStatus.from(stages_query, :sg)
......
...@@ -46,10 +46,10 @@ module Ci ...@@ -46,10 +46,10 @@ module Ci
end end
def has_warnings? def has_warnings?
if @warnings.nil? if @warnings.is_a?(Integer)
statuses.latest.failed_but_allowed.any? @warnings > 0
else else
@warnings statuses.latest.failed_but_allowed.any?
end end
end end
end end
......
...@@ -63,6 +63,7 @@ module Issuable ...@@ -63,6 +63,7 @@ module Issuable
scope :authored, ->(user) { where(author_id: user) } scope :authored, ->(user) { where(author_id: user) }
scope :assigned_to, ->(u) { where(assignee_id: u.id)} scope :assigned_to, ->(u) { where(assignee_id: u.id)}
scope :recent, -> { reorder(id: :desc) } scope :recent, -> { reorder(id: :desc) }
scope :order_position_asc, -> { reorder(position: :asc) }
scope :assigned, -> { where("assignee_id IS NOT NULL") } scope :assigned, -> { where("assignee_id IS NOT NULL") }
scope :unassigned, -> { where("assignee_id IS NULL") } scope :unassigned, -> { where("assignee_id IS NULL") }
scope :of_projects, ->(ids) { where(project_id: ids) } scope :of_projects, ->(ids) { where(project_id: ids) }
...@@ -144,6 +145,7 @@ module Issuable ...@@ -144,6 +145,7 @@ module Issuable
when 'downvotes_desc' then order_downvotes_desc when 'downvotes_desc' then order_downvotes_desc
when 'upvotes_desc' then order_upvotes_desc when 'upvotes_desc' then order_upvotes_desc
when 'priority' then order_labels_priority(excluded_labels: excluded_labels) when 'priority' then order_labels_priority(excluded_labels: excluded_labels)
when 'position_asc' then order_position_asc
else else
order_by(method) order_by(method)
end end
......
class IssueTrackerService < Service class IssueTrackerService < Service
validate :one_issue_tracker, if: :activated?, on: :manual_change
default_value_for :category, 'issue_tracker' default_value_for :category, 'issue_tracker'
# Pattern used to extract links from comments # Pattern used to extract links from comments
...@@ -92,4 +94,13 @@ class IssueTrackerService < Service ...@@ -92,4 +94,13 @@ class IssueTrackerService < Service
def issues_tracker def issues_tracker
Gitlab.config.issues_tracker[to_param] Gitlab.config.issues_tracker[to_param]
end 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 end
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.devise-errors .devise-errors
= devise_error_messages! = devise_error_messages!
.form-group .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." = f.text_field :name, class: "form-control top", required: true, title: "This field is required."
.username.form-group .username.form-group
= f.label :username = f.label :username
......
...@@ -14,13 +14,13 @@ ...@@ -14,13 +14,13 @@
%td.new_line.diff-line-num %td.new_line.diff-line-num
%td.line_content.match= line.text %td.line_content.match= line.text
- else - 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 - link_text = type == "new" ? " " : line.old_pos
- if plain - if plain
= link_text = link_text
- else - else
%a{ href: "##{line_code}", data: { linenumber: link_text } } %a{ href: "##{line_code}", data: { linenumber: link_text } }
- if discussion && !plain - if discussion && discussion.resolvable? && !plain
%diff-note-avatars{ "discussion-id" => discussion.id } %diff-note-avatars{ "discussion-id" => discussion.id }
%td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
- link_text = type == "old" ? " " : line.new_pos - link_text = type == "old" ? " " : line.new_pos
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
- left_position = diff_file.position(left) - left_position = diff_file.position(left)
%td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } } %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 } } %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 } %diff-note-avatars{ "discussion-id" => discussion_left.id }
%td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text) %td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text)
- else - else
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
- right_position = diff_file.position(right) - right_position = diff_file.position(right)
%td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } } %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 } } %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 } %diff-note-avatars{ "discussion-id" => discussion_right.id }
%td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text) %td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text)
- else - else
......
...@@ -62,24 +62,25 @@ ...@@ -62,24 +62,25 @@
- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
.row-content-block{ class: (is_footer ? "footer-block" : "middle-block") } .row-content-block{ class: (is_footer ? "footer-block" : "middle-block") }
- if issuable.new_record? .pull-right
= form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' - if issuable.new_record?
- else = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
= form.submit 'Save changes', class: 'btn btn-save' - 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)) - 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 Please review the
%strong= link_to('contribution guidelines', guide_url) %strong= link_to('contribution guidelines', guide_url)
for this project. 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 = form.hidden_field :lock_version
...@@ -14,18 +14,18 @@ ...@@ -14,18 +14,18 @@
.scroll-container .scroll-container
%ul.tokens-container.list-unstyled %ul.tokens-container.list-unstyled
%li.input-token %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') = icon('filter')
%button.clear-search.hidden{ type: 'button' } %button.clear-search.hidden{ type: 'button' }
= icon('times') = icon('times')
#js-dropdown-hint.dropdown-menu.hint-dropdown #js-dropdown-hint.dropdown-menu.hint-dropdown
%ul{ 'data-dropdown' => true } %ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ 'data-action' => 'submit' } %li.filter-dropdown-item{ data: { action: 'submit' } }
%button.btn.btn-link %button.btn.btn-link
= icon('search') = icon('search')
%span %span
Keep typing and press Enter 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 %li.filter-dropdown-item
%button.btn.btn-link %button.btn.btn-link
-# Encapsulate static class name `{{icon}}` inside #{} to bypass -# Encapsulate static class name `{{icon}}` inside #{} to bypass
...@@ -36,50 +36,50 @@ ...@@ -36,50 +36,50 @@
%span.js-filter-tag.dropdown-light-content %span.js-filter-tag.dropdown-light-content
{{tag}} {{tag}}
#js-dropdown-author.dropdown-menu{ data: { icon: 'pencil', hint: 'author', tag: '@author' } } #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 %li.filter-dropdown-item
%button.btn.btn-link.dropdown-user %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 .dropdown-user-details
%span %span
{{name}} {{name}}
%span.dropdown-light-content %span.dropdown-light-content
@{{username}} @{{username}}
#js-dropdown-assignee.dropdown-menu{ data: { icon: 'user', hint: 'assignee', tag: '@assignee' } } #js-dropdown-assignee.dropdown-menu{ data: { icon: 'user', hint: 'assignee', tag: '@assignee' } }
%ul{ 'data-dropdown' => true } %ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ 'data-value' => 'none' } %li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link %button.btn.btn-link
No Assignee No Assignee
%li.divider %li.divider
%ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item %li.filter-dropdown-item
%button.btn.btn-link.dropdown-user %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 .dropdown-user-details
%span %span
{{name}} {{name}}
%span.dropdown-light-content %span.dropdown-light-content
@{{username}} @{{username}}
#js-dropdown-milestone.dropdown-menu{ data: { icon: 'clock-o', hint: 'milestone', tag: '%milestone' } } #js-dropdown-milestone.dropdown-menu{ data: { icon: 'clock-o', hint: 'milestone', tag: '%milestone' } }
%ul{ 'data-dropdown' => true } %ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ 'data-value' => 'none' } %li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link %button.btn.btn-link
No Milestone No Milestone
%li.filter-dropdown-item{ 'data-value' => 'upcoming' } %li.filter-dropdown-item{ data: { value: 'upcoming' } }
%button.btn.btn-link %button.btn.btn-link
Upcoming Upcoming
%li.divider %li.divider
%ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item %li.filter-dropdown-item
%button.btn.btn-link.js-data-value %button.btn.btn-link.js-data-value
{{title}} {{title}}
#js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label' } } #js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label' } }
%ul{ 'data-dropdown' => true } %ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ 'data-value' => 'none' } %li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link %button.btn.btn-link
No Label No Label
%li.divider %li.divider
%ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item %li.filter-dropdown-item
%button.btn.btn-link %button.btn.btn-link
%span.dropdown-label-box{ style: 'background: {{color}}' } %span.dropdown-label-box{ style: 'background: {{color}}' }
......
...@@ -13,6 +13,6 @@ ...@@ -13,6 +13,6 @@
- class_prefix = dom_class(issuables).pluralize - class_prefix = dom_class(issuables).pluralize
%ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id } %ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id }
= render partial: 'shared/milestones/issuable', = render partial: 'shared/milestones/issuable',
collection: issuables.sort_by(&:position), collection: issuables.order_position_asc,
as: :issuable, as: :issuable,
locals: { show_project_name: show_project_name, show_full_project_name: show_full_project_name } locals: { show_project_name: show_project_name, show_full_project_name: show_full_project_name }
...@@ -10,7 +10,7 @@ class AuthorizedProjectsWorker ...@@ -10,7 +10,7 @@ class AuthorizedProjectsWorker
end end
def self.bulk_perform_async(args_list) def self.bulk_perform_async(args_list)
Sidekiq::Client.push_bulk('class' => self, 'args' => args_list) Sidekiq::Client.push_bulk('class' => self, 'queue' => sidekiq_options['queue'], 'args' => args_list)
end end
def perform(user_id) def perform(user_id)
......
---
title: Prevent builds dropdown to close when the user clicks in a build
merge_request:
author:
---
title: allow offset query parameter for infinite list pages
merge_request:
author:
---
title: Order milestone issues by position ascending in api
merge_request: 9635
author: George Andrinopoulos
---
title: Fix create issue form buttons are misaligned on mobile
merge_request: 9706
author: TM Lee
---
title: Fix GitHub Import deleting branches for open PRs from a fork
merge_request: 9758
author:
---
title: Make authorized projects worker use a specific queue instead of the default one
merge_request: 9813
author:
---
title: Change label for name on sign up form
merge_request:
author:
---
title: ensure MR widget dropdown is same color as button
merge_request:
author:
---
title: Prevent more than one issue tracker to be active for the same project
merge_request:
author: luisdgs19
---
title: Add frequently used emojis back to awards menu
merge_request:
author:
---
title: Show members of parent groups on project members page
merge_request:
author:
---
title: Enable snippets for new projects by default
merge_request:
author:
---
title: Fix "passed with warnings" stage status on MySQL installations
merge_request: 9802
author:
---
title: Fix for creating a project through API when import_url is nil
merge_request: 9841
author:
---
title: Update code editor (ACE) to 1.2.6, to fix input problems with compose key
merge_request:
author:
...@@ -89,7 +89,7 @@ production: &base ...@@ -89,7 +89,7 @@ production: &base
issues: true issues: true
merge_requests: true merge_requests: true
wiki: true wiki: true
snippets: false snippets: true
builds: true builds: true
container_registry: true container_registry: true
...@@ -441,6 +441,16 @@ production: &base ...@@ -441,6 +441,16 @@ production: &base
shared: shared:
# path: /mnt/gitlab # Default: 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 # 4. Advanced settings
......
...@@ -221,7 +221,7 @@ Settings.gitlab['session_expire_delay'] ||= 10080 ...@@ -221,7 +221,7 @@ Settings.gitlab['session_expire_delay'] ||= 10080
Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil? Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil?
Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil? Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil?
Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil?
Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? Settings.gitlab.default_projects_features['snippets'] = true if Settings.gitlab.default_projects_features['snippets'].nil?
Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil?
Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil? Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil?
Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
......
...@@ -13,24 +13,27 @@ def storage_validation_error(message) ...@@ -13,24 +13,27 @@ def storage_validation_error(message)
raise "#{message}. Please fix this in your gitlab.yml before starting GitLab." raise "#{message}. Please fix this in your gitlab.yml before starting GitLab."
end end
def validate_storages def validate_storages_config
storage_validation_error('No repository storage path defined') if Gitlab.config.repositories.storages.empty? storage_validation_error('No repository storage path defined') if Gitlab.config.repositories.storages.empty?
Gitlab.config.repositories.storages.each do |name, repository_storage| Gitlab.config.repositories.storages.each do |name, repository_storage|
storage_validation_error("\"#{name}\" is not a valid storage name") unless storage_name_valid?(name) storage_validation_error("\"#{name}\" is not a valid storage name") unless storage_name_valid?(name)
if repository_storage.is_a?(String) if repository_storage.is_a?(String)
error = "#{name} is not a valid storage, because it has no `path` key. " \ raise "#{name} is not a valid storage, because it has no `path` key. " \
"It may be configured as:\n\n#{name}:\n path: #{repository_storage}\n\n" \ "It may be configured as:\n\n#{name}:\n path: #{repository_storage}\n\n" \
"Refer to gitlab.yml.example for an updated example" "For source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.\n\n" \
"If you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.\n"
storage_validation_error(error)
end end
if !repository_storage.is_a?(Hash) || repository_storage['path'].nil? if !repository_storage.is_a?(Hash) || repository_storage['path'].nil?
storage_validation_error("#{name} is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example") storage_validation_error("#{name} is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example")
end end
end
end
def validate_storages_paths
Gitlab.config.repositories.storages.each do |name, repository_storage|
parent_name, _parent_path = find_parent_path(name, repository_storage['path']) parent_name, _parent_path = find_parent_path(name, repository_storage['path'])
if parent_name if parent_name
storage_validation_error("#{name} is a nested path of #{parent_name}. Nested paths are not supported for repository storages") storage_validation_error("#{name} is a nested path of #{parent_name}. Nested paths are not supported for repository storages")
...@@ -38,4 +41,5 @@ def validate_storages ...@@ -38,4 +41,5 @@ def validate_storages
end end
end end
validate_storages unless Rails.env.test? || ENV['SKIP_STORAGE_VALIDATION'] == 'true' validate_storages_config
validate_storages_paths unless Rails.env.test? || ENV['SKIP_STORAGE_VALIDATION'] == 'true'
...@@ -133,8 +133,7 @@ var config = { ...@@ -133,8 +133,7 @@ var config = {
extensions: ['.js', '.es6', '.js.es6'], extensions: ['.js', '.es6', '.js.es6'],
alias: { alias: {
'~': path.join(ROOT_PATH, 'app/assets/javascripts'), '~': path.join(ROOT_PATH, 'app/assets/javascripts'),
'emoji-map$': path.join(ROOT_PATH, 'fixtures/emojis/digests.json'), 'emojis': path.join(ROOT_PATH, 'fixtures/emojis'),
'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'),
'empty_states': path.join(ROOT_PATH, 'app/views/shared/empty_states'), 'empty_states': path.join(ROOT_PATH, 'app/views/shared/empty_states'),
'icons': path.join(ROOT_PATH, 'app/views/shared/icons'), 'icons': path.join(ROOT_PATH, 'app/views/shared/icons'),
'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'),
......
...@@ -17,14 +17,17 @@ Pages to the latest supported version. ...@@ -17,14 +17,17 @@ Pages to the latest supported version.
## Prerequisites ## Prerequisites
Before proceeding with the Pages configuration, you will need to: Before proceeding with the Pages configuration, make sure that:
1. Have a separate domain under which the GitLab Pages will be served. In this 1. You have a separate domain under which GitLab Pages will be served. In
document we assume that to be `example.io`. this document we assume that to be `example.io`.
1. Configure a **wildcard DNS record**. 1. You have configured a **wildcard DNS record** for that domain.
1. (Optional) Have a **wildcard certificate** for that domain if you decide to 1. You have installed the `zip` and `unzip` packages in the same server that
serve Pages under HTTPS. GitLab is installed since they are needed to compress/uncompress the
1. (Optional but recommended) Enable [Shared runners](../../ci/runners/README.md) 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. so that your users don't have to bring their own.
### DNS configuration ### DNS configuration
...@@ -390,3 +393,4 @@ than GitLab to prevent XSS attacks. ...@@ -390,3 +393,4 @@ than GitLab to prevent XSS attacks.
[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure [reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[restart]: ../restart_gitlab.md#installations-from-source [restart]: ../restart_gitlab.md#installations-from-source
[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4 [gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4
[shared runners]: ../../ci/runners/README.md
...@@ -27,6 +27,8 @@ ...@@ -27,6 +27,8 @@
## Breaking changes ## Breaking changes
- [CI variables renaming](variables/README.md#9-0-renaming) Read about the
deprecated CI variables and what you should use for GitLab 9.0+.
- [New CI job permissions model](../user/project/new_ci_build_permissions_model.md) - [New CI job permissions model](../user/project/new_ci_build_permissions_model.md)
Read about what changed in GitLab 8.12 and how that affects your jobs. Read about what changed in GitLab 8.12 and how that affects your jobs.
There's a new way to access your Git submodules and LFS objects in jobs. There's a new way to access your Git submodules and LFS objects in jobs.
...@@ -6,7 +6,7 @@ projects. ...@@ -6,7 +6,7 @@ projects.
GitLab offers a [continuous integration][ci] service. If you GitLab offers a [continuous integration][ci] service. If you
[add a `.gitlab-ci.yml` file][yaml] to the root directory of your repository, [add a `.gitlab-ci.yml` file][yaml] to the root directory of your repository,
and configure your GitLab project to use a [Runner], then each merge request or and configure your GitLab project to use a [Runner], then each commit or
push, triggers your CI [pipeline]. push, triggers your CI [pipeline].
The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it runs The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it runs
...@@ -14,8 +14,8 @@ a pipeline with three [stages]: `build`, `test`, and `deploy`. You don't need to ...@@ -14,8 +14,8 @@ a pipeline with three [stages]: `build`, `test`, and `deploy`. You don't need to
use all three stages; stages with no jobs are simply ignored. use all three stages; stages with no jobs are simply ignored.
If everything runs OK (no non-zero return values), you'll get a nice green If everything runs OK (no non-zero return values), you'll get a nice green
checkmark associated with the pushed commit or merge request. This makes it checkmark associated with the commit. This makes it
easy to see whether a merge request caused any of the tests to fail before easy to see whether a commit caused any of the tests to fail before
you even look at the code. you even look at the code.
Most projects use GitLab's CI service to run the test suite so that Most projects use GitLab's CI service to run the test suite so that
......
This diff is collapsed.
...@@ -16,6 +16,22 @@ minification, and compression of our assets. ...@@ -16,6 +16,22 @@ minification, and compression of our assets.
[jQuery][jquery] is used throughout the application's JavaScript, with [jQuery][jquery] is used throughout the application's JavaScript, with
[Vue.js][vue] for particularly advanced, dynamic elements. [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 ### Vue
For more complex frontend features, we recommend using Vue.js. It shares For more complex frontend features, we recommend using Vue.js. It shares
...@@ -238,8 +254,8 @@ readability. ...@@ -238,8 +254,8 @@ readability.
See the relevant style guides for our guidelines and for information on linting: See the relevant style guides for our guidelines and for information on linting:
- [SCSS][scss-style-guide] - [SCSS][scss-style-guide]
- JavaScript - We defer to [AirBnb][airbnb-js-style-guide] on most style-related - JavaScript - We defer to [AirBnb][airbnb-js-style-guide] on most style-related
conventions and enforce them with eslint. See [our current .eslintrc][eslistrc] conventions and enforce them with eslint. See [our current .eslintrc][eslintrc]
for specific rules and patterns. for specific rules and patterns.
## Testing ## Testing
...@@ -439,3 +455,5 @@ Scenario: Developer can approve merge request ...@@ -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 [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 [airbnb-js-style-guide]: https://github.com/airbnb/javascript
[eslintrc]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc [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
...@@ -167,6 +167,15 @@ A **comment** is a written piece of text that users of GitLab can create. Commen ...@@ -167,6 +167,15 @@ A **comment** is a written piece of text that users of GitLab can create. Commen
#### Discussion #### 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. 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]. 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].
......
# Installing a locally compiled google-protobuf gem
First we must find the exact version of google-protobuf that your
GitLab installation requires.
cd /home/git/gitlab
# Only one of the following two commands will print something. It
# will look like: * google-protobuf (3.2.0)
bundle list | grep google-protobuf
bundle check | grep google-protobuf
Below we use `3.2.0` as an example. Replace it with the version number
you found above.
cd /home/git/gitlab
sudo -u git -H gem install google-protobuf --version 3.2.0 --platform ruby
Finally, you can test whether google-protobuf loads correctly. The
following should print 'OK'.
sudo -u git -H bundle exec ruby -rgoogle/protobuf -e 'puts :OK'
If the `gem install` command fails you may need to install developer
tools. On Debian: `apt-get install build-essential libgmp-dev`, on
Centos/RedHat `yum groupinstall 'Development Tools'`.
...@@ -288,9 +288,9 @@ sudo usermod -aG redis git ...@@ -288,9 +288,9 @@ sudo usermod -aG redis git
### Clone the Source ### Clone the Source
# Clone GitLab repository # 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 ### Configure It
...@@ -658,6 +658,12 @@ misconfigured gitlab-workhorse instance. Double-check that you've ...@@ -658,6 +658,12 @@ misconfigured gitlab-workhorse instance. Double-check that you've
[installed Go](#3-go), [installed gitlab-workhorse](#install-gitlab-workhorse), [installed Go](#3-go), [installed gitlab-workhorse](#install-gitlab-workhorse),
and correctly [configured Nginx](#site-configuration). and correctly [configured Nginx](#site-configuration).
### google-protobuf "LoadError: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.14' not found"
This can happen on some platforms for some versions of the
google-protobuf gem. The workaround is to [install a source-only
version of this gem](google-protobuf.md).
[RVM]: https://rvm.io/ "RVM Homepage" [RVM]: https://rvm.io/ "RVM Homepage"
[rbenv]: https://github.com/sstephenson/rbenv "rbenv on GitHub" [rbenv]: https://github.com/sstephenson/rbenv "rbenv on GitHub"
[chruby]: https://github.com/postmodern/chruby "chruby on GitHub" [chruby]: https://github.com/postmodern/chruby "chruby on GitHub"
# 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 #### Configuration changes for repository storages
This version introduces a new configuration structure for repository storages. This version introduces a new configuration structure for repository storages.
...@@ -85,3 +244,78 @@ via [/etc/default/gitlab]. ...@@ -85,3 +244,78 @@ via [/etc/default/gitlab].
[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache [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 [/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
...@@ -116,7 +116,8 @@ module API ...@@ -116,7 +116,8 @@ module API
finder_params = { finder_params = {
project_id: user_project.id, project_id: user_project.id,
milestone_title: milestone.title milestone_title: milestone.title,
sort: 'position_asc'
} }
issues = IssuesFinder.new(current_user, finder_params).execute issues = IssuesFinder.new(current_user, finder_params).execute
...@@ -138,7 +139,8 @@ module API ...@@ -138,7 +139,8 @@ module API
finder_params = { finder_params = {
project_id: user_project.id, 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 merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute
......
module Gitlab
module Ci
module Status
module Pipeline
class Blocked < SimpleDelegator
include Status::Extended
def text
'blocked'
end
def label
'waiting for manual action'
end
def self.matches?(pipeline, user)
pipeline.blocked?
end
end
end
end
end
end
...@@ -4,7 +4,8 @@ module Gitlab ...@@ -4,7 +4,8 @@ module Gitlab
module Pipeline module Pipeline
class Factory < Status::Factory class Factory < Status::Factory
def self.extended_statuses def self.extended_statuses
[Status::SuccessWarning] [[Status::SuccessWarning,
Status::Pipeline::Blocked]]
end end
def self.common_helpers def self.common_helpers
......
...@@ -37,7 +37,7 @@ module Gitlab ...@@ -37,7 +37,7 @@ module Gitlab
def get_etag(env) def get_etag(env)
cache_key = env['PATH_INFO'] cache_key = env['PATH_INFO']
store = Store.new store = Gitlab::EtagCaching::Store.new
current_value = store.get(cache_key) current_value = store.get(cache_key)
cached_value_present = current_value.present? cached_value_present = current_value.present?
......
...@@ -171,6 +171,8 @@ module Gitlab ...@@ -171,6 +171,8 @@ module Gitlab
end end
def clean_up_restored_branches(pull_request) def clean_up_restored_branches(pull_request)
return if pull_request.opened?
remove_branch(pull_request.source_branch_name) unless pull_request.source_branch_exists? remove_branch(pull_request.source_branch_name) unless pull_request.source_branch_exists?
remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists? remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists?
end end
......
...@@ -60,6 +60,10 @@ module Gitlab ...@@ -60,6 +60,10 @@ module Gitlab
source_branch.repo.id != target_branch.repo.id source_branch.repo.id != target_branch.repo.id
end end
def opened?
state == 'opened'
end
private private
def state def state
......
...@@ -47,7 +47,13 @@ module Gitlab ...@@ -47,7 +47,13 @@ module Gitlab
def group_members def group_members
return [] unless @current_user.can?(:admin_group, @project.group) return [] unless @current_user.can?(:admin_group, @project.group)
MembersFinder.new(@project.project_members, @project.group).execute(@current_user) # We need `.where.not(user_id: nil)` here otherwise when a group has an
# invitee, it would make the following query return 0 rows since a NULL
# user_id would be present in the subquery
# See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
non_null_user_ids = @project.project_members.where.not(user_id: nil).select(:user_id)
GroupMembersFinder.new(@project.group).execute.where.not(user_id: non_null_user_ids)
end end
end end
end end
......
...@@ -9,6 +9,8 @@ module Gitlab ...@@ -9,6 +9,8 @@ module Gitlab
end end
def self.valid?(url) def self.valid?(url)
return false unless url
Addressable::URI.parse(url.strip) Addressable::URI.parse(url.strip)
true true
......
...@@ -62,7 +62,7 @@ namespace :gitlab do ...@@ -62,7 +62,7 @@ namespace :gitlab do
ref = Shellwords.escape(args[:ref]) 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 } .map { |file| Rails.root.join(file.strip).to_s }
.select { |file| File.file?(file) } .select { |file| File.file?(file) }
......
...@@ -40,6 +40,14 @@ FactoryGirl.define do ...@@ -40,6 +40,14 @@ FactoryGirl.define do
trait :invalid do trait :invalid do
config(rspec: nil) config(rspec: nil)
end end
trait :blocked do
status :manual
end
trait :success do
status :success
end
end end
end end
end end
...@@ -23,6 +23,56 @@ feature 'Diff note avatars', feature: true, js: true do ...@@ -23,6 +23,56 @@ feature 'Diff note avatars', feature: true, js: true do
login_as user login_as user
end 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| %w(inline parallel).each do |view|
context "#{view} view" do context "#{view} view" do
before do before do
......
...@@ -21,36 +21,28 @@ feature 'Project edit', feature: true, js: true do ...@@ -21,36 +21,28 @@ feature 'Project edit', feature: true, js: true do
expect(page).to have_selector('.merge-requests-feature', visible: false) expect(page).to have_selector('.merge-requests-feature', visible: false)
end end
it 'hides merge requests section after save' do context 'given project with merge_requests_disabled access level' do
select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level') let(:project) { create(:project, :merge_requests_disabled) }
expect(page).to have_selector('.merge-requests-feature', visible: false)
click_button 'Save changes'
wait_for_ajax it 'hides merge requests section' do
expect(page).to have_selector('.merge-requests-feature', visible: false)
expect(page).to have_selector('.merge-requests-feature', visible: false) end
end end
end end
context 'builds select' do 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') select('Disabled', from: 'project_project_feature_attributes_builds_access_level')
expect(page).to have_selector('.builds-feature', visible: false) expect(page).to have_selector('.builds-feature', visible: false)
end end
it 'hides merge requests section after save' do context 'given project with builds_disabled access level' do
select('Disabled', from: 'project_project_feature_attributes_builds_access_level') let(:project) { create(:project, :builds_disabled) }
expect(page).to have_selector('.builds-feature', visible: false)
click_button 'Save changes' it 'hides builds select section' do
expect(page).to have_selector('.builds-feature', visible: false)
wait_for_ajax end
expect(page).to have_selector('.builds-feature', visible: false)
end end
end end
end end
......
require 'spec_helper'
describe MembersFinder, '#execute' do
let(:group) { create(:group) }
let(:nested_group) { create(:group, :access_requestable, parent: group) }
let(:project) { create(:project, namespace: nested_group) }
let(:user1) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:user4) { create(:user) }
it 'returns members for project and parent groups' do
nested_group.request_access(user1)
member1 = group.add_master(user2)
member2 = nested_group.add_master(user3)
member3 = project.add_master(user4)
result = described_class.new(project, user2).execute
expect(result.to_a).to eq([member3, member2, member1])
end
end
...@@ -12,63 +12,77 @@ describe '6_validations', lib: true do ...@@ -12,63 +12,77 @@ describe '6_validations', lib: true do
FileUtils.rm_rf('tmp/tests/paths') FileUtils.rm_rf('tmp/tests/paths')
end end
context 'with correct settings' do describe 'validate_storages_config' do
before do context 'with correct settings' do
mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/d' }) before do
end mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/d' })
end
it 'passes through' do it 'passes through' do
expect { validate_storages }.not_to raise_error expect { validate_storages_config }.not_to raise_error
end
end end
end
context 'with invalid storage names' do context 'with invalid storage names' do
before do before do
mock_storages('name with spaces' => { 'path' => 'tmp/tests/paths/a/b/c' }) mock_storages('name with spaces' => { 'path' => 'tmp/tests/paths/a/b/c' })
end end
it 'throws an error' do it 'throws an error' do
expect { validate_storages }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.') expect { validate_storages_config }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.')
end
end end
end
context 'with nested storage paths' do context 'with incomplete settings' do
before do before do
mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c/d' }) mock_storages('foo' => {})
end end
it 'throws an error' do it 'throws an error suggesting the user to update its settings' do
expect { validate_storages }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.') expect { validate_storages_config }.to raise_error('foo is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example. Please fix this in your gitlab.yml before starting GitLab.')
end
end end
end
context 'with similar but un-nested storage paths' do context 'with deprecated settings structure' do
before do before do
mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c2' }) mock_storages('foo' => 'tmp/tests/paths/a/b/c')
end end
it 'passes through' do it 'throws an error suggesting the user to update its settings' do
expect { validate_storages }.not_to raise_error expect { validate_storages_config }.to raise_error("foo is not a valid storage, because it has no `path` key. It may be configured as:\n\nfoo:\n path: tmp/tests/paths/a/b/c\n\nFor source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.\n\nIf you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.\n")
end
end end
end end
context 'with incomplete settings' do describe 'validate_storages_paths' do
before do context 'with correct settings' do
mock_storages('foo' => {}) before do
end mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/d' })
end
it 'throws an error suggesting the user to update its settings' do it 'passes through' do
expect { validate_storages }.to raise_error('foo is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example. Please fix this in your gitlab.yml before starting GitLab.') expect { validate_storages_paths }.not_to raise_error
end
end end
end
context 'with deprecated settings structure' do context 'with nested storage paths' do
before do before do
mock_storages('foo' => 'tmp/tests/paths/a/b/c') mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c/d' })
end
it 'throws an error' do
expect { validate_storages_paths }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.')
end
end end
it 'throws an error suggesting the user to update its settings' do context 'with similar but un-nested storage paths' do
expect { validate_storages }.to raise_error("foo is not a valid storage, because it has no `path` key. It may be configured as:\n\nfoo:\n path: tmp/tests/paths/a/b/c\n\nRefer to gitlab.yml.example for an updated example. Please fix this in your gitlab.yml before starting GitLab.") before do
mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c2' })
end
it 'passes through' do
expect { validate_storages_paths }.not_to raise_error
end
end end
end end
......
/* 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 */ /* 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 Cookies from 'js-cookie';
import AwardsHandler from '~/awards_handler';
const AwardsHandler = require('~/awards_handler'); promisePolyfill.polyfill();
(function() { (function() {
var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu; var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu;
...@@ -207,8 +209,8 @@ const AwardsHandler = require('~/awards_handler'); ...@@ -207,8 +209,8 @@ const AwardsHandler = require('~/awards_handler');
expect($('[data-name=alien]').is(':visible')).toBe(true); expect($('[data-name=alien]').is(':visible')).toBe(true);
}) })
.then(done) .then(done)
.catch(() => { .catch((err) => {
done.fail('Failed to open and build emoji menu'); done.fail(`Failed to open and build emoji menu: ${err.message}`);
}); });
}); });
}); });
...@@ -231,8 +233,8 @@ const AwardsHandler = require('~/awards_handler'); ...@@ -231,8 +233,8 @@ const AwardsHandler = require('~/awards_handler');
it('should add selected emoji to awards block', function(done) { it('should add selected emoji to awards block', function(done) {
return openEmojiMenuAndAddEmoji() return openEmojiMenuAndAddEmoji()
.then(done) .then(done)
.catch(() => { .catch((err) => {
done.fail('Failed to open and build emoji menu'); done.fail(`Failed to open and build emoji menu: ${err.message}`);
}); });
}); });
it('should remove already selected emoji', function(done) { it('should remove already selected emoji', function(done) {
...@@ -246,7 +248,46 @@ const AwardsHandler = require('~/awards_handler'); ...@@ -246,7 +248,46 @@ const AwardsHandler = require('~/awards_handler');
}) })
.then(done) .then(done)
.catch((err) => { .catch((err) => {
done.fail('Failed to open and build emoji menu'); done.fail(`Failed to open and build emoji menu: ${err.message}`);
});
});
});
describe('frequently used emojis', function() {
beforeEach(() => {
// Clear it out
Cookies.set('frequently_used_emojis', '');
});
it('shouldn\'t have any "Frequently used" heading if no frequently used emojis', function(done) {
return openAndWaitForEmojiMenu()
.then(() => {
const emojiMenu = document.querySelector('.emoji-menu');
Array.prototype.forEach.call(emojiMenu.querySelectorAll('.emoji-menu-title'), (title) => {
expect(title.textContent.trim().toLowerCase()).not.toBe('frequently used');
});
})
.then(done)
.catch((err) => {
done.fail(`Failed to open and build emoji menu: ${err.message}`);
});
});
it('should have any frequently used section when there are frequently used emojis', function(done) {
awardsHandler.addEmojiToFrequentlyUsedList('8ball');
return openAndWaitForEmojiMenu()
.then(() => {
const emojiMenu = document.querySelector('.emoji-menu');
const hasFrequentlyUsedHeading = Array.prototype.some.call(emojiMenu.querySelectorAll('.emoji-menu-title'), title =>
title.textContent.trim().toLowerCase() === 'frequently used'
);
expect(hasFrequentlyUsedHeading).toBe(true);
})
.then(done)
.catch((err) => {
done.fail(`Failed to open and build emoji menu: ${err.message}`);
}); });
}); });
}); });
......
import '~/extensions/string';
import '~/extensions/array';
require('~/extensions/string'); import { glEmojiTag } from '~/behaviors/gl_emoji';
require('~/extensions/array'); import {
isEmojiUnicodeSupported,
const glEmoji = require('~/behaviors/gl_emoji'); isFlagEmoji,
isKeycapEmoji,
const glEmojiTag = glEmoji.glEmojiTag; isSkinToneComboEmoji,
const isEmojiUnicodeSupported = glEmoji.isEmojiUnicodeSupported; isHorceRacingSkinToneComboEmoji,
const isFlagEmoji = glEmoji.isFlagEmoji; isPersonZwjEmoji,
const isKeycapEmoji = glEmoji.isKeycapEmoji; } from '~/behaviors/gl_emoji/is_emoji_unicode_supported';
const isSkinToneComboEmoji = glEmoji.isSkinToneComboEmoji;
const isHorceRacingSkinToneComboEmoji = glEmoji.isHorceRacingSkinToneComboEmoji;
const isPersonZwjEmoji = glEmoji.isPersonZwjEmoji;
const emptySupportMap = { const emptySupportMap = {
personZwj: false, personZwj: false,
......
/* eslint-disable no-new */ /* eslint-disable no-new */
require('~/flash'); import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
require('~/mini_pipeline_graph_dropdown'); import '~/flash';
(() => { (() => {
describe('Mini Pipeline Graph Dropdown', () => { describe('Mini Pipeline Graph Dropdown', () => {
...@@ -13,7 +13,7 @@ require('~/mini_pipeline_graph_dropdown'); ...@@ -13,7 +13,7 @@ require('~/mini_pipeline_graph_dropdown');
describe('When is initialized', () => { describe('When is initialized', () => {
it('should initialize without errors when no options are given', () => { 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'); expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container');
}); });
...@@ -21,7 +21,7 @@ require('~/mini_pipeline_graph_dropdown'); ...@@ -21,7 +21,7 @@ require('~/mini_pipeline_graph_dropdown');
it('should set the container as the given prop', () => { it('should set the container as the given prop', () => {
const container = '.foo'; const container = '.foo';
const miniPipelineGraph = new window.gl.MiniPipelineGraph({ container }); const miniPipelineGraph = new MiniPipelineGraph({ container });
expect(miniPipelineGraph.container).toEqual(container); expect(miniPipelineGraph.container).toEqual(container);
}); });
...@@ -29,9 +29,9 @@ require('~/mini_pipeline_graph_dropdown'); ...@@ -29,9 +29,9 @@ require('~/mini_pipeline_graph_dropdown');
describe('When dropdown is clicked', () => { describe('When dropdown is clicked', () => {
it('should call getBuildsList', () => { 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(); document.querySelector('.js-builds-dropdown-button').click();
...@@ -41,11 +41,32 @@ require('~/mini_pipeline_graph_dropdown'); ...@@ -41,11 +41,32 @@ require('~/mini_pipeline_graph_dropdown');
it('should make a request to the endpoint provided in the html', () => { it('should make a request to the endpoint provided in the html', () => {
const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {}); 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(); document.querySelector('.js-builds-dropdown-button').click();
expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar'); 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);
});
}); });
}); });
})(); })();
/* 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');
});
});
});
require 'spec_helper'
describe Gitlab::Ci::Status::Pipeline::Blocked do
let(:pipeline) { double('pipeline') }
subject do
described_class.new(pipeline)
end
describe '#text' do
it 'overrides status text' do
expect(subject.text).to eq 'blocked'
end
end
describe '#label' do
it 'overrides status label' do
expect(subject.label).to eq 'waiting for manual action'
end
end
describe '.matches?' do
let(:user) { double('user') }
subject { described_class.matches?(pipeline, user) }
context 'when pipeline is blocked' do
let(:pipeline) { create(:ci_pipeline, :blocked) }
it 'is a correct match' do
expect(subject).to be true
end
end
context 'when pipeline is not blocked' do
let(:pipeline) { create(:ci_pipeline, :success) }
it 'does not match' do
expect(subject).to be false
end
end
end
end
...@@ -11,7 +11,8 @@ describe Gitlab::Ci::Status::Pipeline::Factory do ...@@ -11,7 +11,8 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
end end
context 'when pipeline has a core status' do context 'when pipeline has a core status' do
HasStatus::AVAILABLE_STATUSES.each do |simple_status| (HasStatus::AVAILABLE_STATUSES - [HasStatus::BLOCKED_STATUS])
.each do |simple_status|
context "when core status is #{simple_status}" do context "when core status is #{simple_status}" do
let(:pipeline) { create(:ci_pipeline, status: simple_status) } let(:pipeline) { create(:ci_pipeline, status: simple_status) }
...@@ -23,7 +24,7 @@ describe Gitlab::Ci::Status::Pipeline::Factory do ...@@ -23,7 +24,7 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
expect(factory.core_status).to be_a expected_status expect(factory.core_status).to be_a expected_status
end end
it 'does not matche extended statuses' do it 'does not match extended statuses' do
expect(factory.extended_statuses).to be_empty expect(factory.extended_statuses).to be_empty
end end
...@@ -39,6 +40,27 @@ describe Gitlab::Ci::Status::Pipeline::Factory do ...@@ -39,6 +40,27 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
end end
end end
end end
context "when core status is manual" do
let(:pipeline) { create(:ci_pipeline, status: :manual) }
it "matches manual core status" do
expect(factory.core_status)
.to be_a Gitlab::Ci::Status::Manual
end
it 'matches a correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Pipeline::Blocked]
end
it 'extends core status with common pipeline methods' do
expect(status).to have_details
expect(status).not_to have_action
expect(status.details_path)
.to include "pipelines/#{pipeline.id}"
end
end
end end
context 'when pipeline has warnings' do context 'when pipeline has warnings' do
......
...@@ -55,9 +55,6 @@ describe Gitlab::GithubImport::Importer, lib: true do ...@@ -55,9 +55,6 @@ describe Gitlab::GithubImport::Importer, lib: true do
allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2]) allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2])
end end
let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
let(:label1) do let(:label1) do
double( double(
name: 'Bug', name: 'Bug',
...@@ -127,32 +124,6 @@ describe Gitlab::GithubImport::Importer, lib: true do ...@@ -127,32 +124,6 @@ describe Gitlab::GithubImport::Importer, lib: true do
) )
end end
let!(:user) { create(:user, email: octocat.email) }
let(:repository) { double(id: 1, fork: false) }
let(:source_sha) { create(:commit, project: project).id }
let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha) }
let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) }
let(:pull_request) do
double(
number: 1347,
milestone: nil,
state: 'open',
title: 'New feature',
body: 'Please pull these awesome changes',
head: source_branch,
base: target_branch,
assignee: nil,
user: octocat,
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
merged_at: nil,
url: "#{api_root}/repos/octocat/Hello-World/pulls/1347",
labels: [double(name: 'Label #2')]
)
end
let(:release1) do let(:release1) do
double( double(
tag_name: 'v1.0.0', tag_name: 'v1.0.0',
...@@ -177,12 +148,14 @@ describe Gitlab::GithubImport::Importer, lib: true do ...@@ -177,12 +148,14 @@ describe Gitlab::GithubImport::Importer, lib: true do
) )
end end
subject { described_class.new(project) }
it 'returns true' do it 'returns true' do
expect(described_class.new(project).execute).to eq true expect(subject.execute).to eq true
end end
it 'does not raise an error' do it 'does not raise an error' do
expect { described_class.new(project).execute }.not_to raise_error expect { subject.execute }.not_to raise_error
end end
it 'stores error messages' do it 'stores error messages' do
...@@ -205,15 +178,93 @@ describe Gitlab::GithubImport::Importer, lib: true do ...@@ -205,15 +178,93 @@ describe Gitlab::GithubImport::Importer, lib: true do
end end
end end
shared_examples 'Gitlab::GithubImport unit-testing' do
describe '#clean_up_restored_branches' do
subject { described_class.new(project) }
before do
allow(gh_pull_request).to receive(:source_branch_exists?).at_least(:once) { false }
allow(gh_pull_request).to receive(:target_branch_exists?).at_least(:once) { false }
end
context 'when pull request stills open' do
let(:gh_pull_request) { Gitlab::GithubImport::PullRequestFormatter.new(project, pull_request) }
it 'does not remove branches' do
expect(subject).not_to receive(:remove_branch)
subject.send(:clean_up_restored_branches, gh_pull_request)
end
end
context 'when pull request is closed' do
let(:gh_pull_request) { Gitlab::GithubImport::PullRequestFormatter.new(project, closed_pull_request) }
it 'does remove branches' do
expect(subject).to receive(:remove_branch).at_least(2).times
subject.send(:clean_up_restored_branches, gh_pull_request)
end
end
end
end
let(:project) { create(:project, :wiki_disabled, import_url: "#{repo_root}/octocat/Hello-World.git") } let(:project) { create(:project, :wiki_disabled, import_url: "#{repo_root}/octocat/Hello-World.git") }
let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
let(:credentials) { { user: 'joe' } } let(:credentials) { { user: 'joe' } }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
let(:repository) { double(id: 1, fork: false) }
let(:source_sha) { create(:commit, project: project).id }
let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha) }
let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) }
let(:pull_request) do
double(
number: 1347,
milestone: nil,
state: 'open',
title: 'New feature',
body: 'Please pull these awesome changes',
head: source_branch,
base: target_branch,
assignee: nil,
user: octocat,
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
merged_at: nil,
url: "#{api_root}/repos/octocat/Hello-World/pulls/1347",
labels: [double(name: 'Label #2')]
)
end
let(:closed_pull_request) do
double(
number: 1347,
milestone: nil,
state: 'closed',
title: 'New feature',
body: 'Please pull these awesome changes',
head: source_branch,
base: target_branch,
assignee: nil,
user: octocat,
created_at: created_at,
updated_at: updated_at,
closed_at: updated_at,
merged_at: nil,
url: "#{api_root}/repos/octocat/Hello-World/pulls/1347",
labels: [double(name: 'Label #2')]
)
end
context 'when importing a GitHub project' do context 'when importing a GitHub project' do
let(:api_root) { 'https://api.github.com' } let(:api_root) { 'https://api.github.com' }
let(:repo_root) { 'https://github.com' } let(:repo_root) { 'https://github.com' }
subject { described_class.new(project) }
it_behaves_like 'Gitlab::GithubImport::Importer#execute' it_behaves_like 'Gitlab::GithubImport::Importer#execute'
it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs' it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs'
it_behaves_like 'Gitlab::GithubImport unit-testing'
describe '#client' do describe '#client' do
it 'instantiates a Client' do it 'instantiates a Client' do
...@@ -223,7 +274,7 @@ describe Gitlab::GithubImport::Importer, lib: true do ...@@ -223,7 +274,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
{} {}
) )
described_class.new(project).client subject.client
end end
end end
end end
...@@ -231,6 +282,8 @@ describe Gitlab::GithubImport::Importer, lib: true do ...@@ -231,6 +282,8 @@ describe Gitlab::GithubImport::Importer, lib: true do
context 'when importing a Gitea project' do context 'when importing a Gitea project' do
let(:api_root) { 'https://try.gitea.io/api/v1' } let(:api_root) { 'https://try.gitea.io/api/v1' }
let(:repo_root) { 'https://try.gitea.io' } let(:repo_root) { 'https://try.gitea.io' }
subject { described_class.new(project) }
before do before do
project.update(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git") project.update(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git")
end end
...@@ -239,6 +292,7 @@ describe Gitlab::GithubImport::Importer, lib: true do ...@@ -239,6 +292,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
let(:expected_not_called) { [:import_releases] } let(:expected_not_called) { [:import_releases] }
end end
it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs' it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs'
it_behaves_like 'Gitlab::GithubImport unit-testing'
describe '#client' do describe '#client' do
it 'instantiates a Client' do it 'instantiates a Client' do
...@@ -248,7 +302,7 @@ describe Gitlab::GithubImport::Importer, lib: true do ...@@ -248,7 +302,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
{ host: "#{repo_root}:443/foo", api_version: 'v1' } { host: "#{repo_root}:443/foo", api_version: 'v1' }
) )
described_class.new(project).client subject.client
end end
end end
end end
......
...@@ -306,4 +306,12 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do ...@@ -306,4 +306,12 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
expect(pull_request.url).to eq 'https://api.github.com/repos/octocat/Hello-World/pulls/1347' expect(pull_request.url).to eq 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
end end
end end
describe '#opened?' do
let(:raw_data) { double(base_data.merge(state: 'open')) }
it 'returns true when state is "open"' do
expect(pull_request.opened?).to be_truthy
end
end
end end
...@@ -70,4 +70,12 @@ describe Gitlab::UrlSanitizer, lib: true do ...@@ -70,4 +70,12 @@ describe Gitlab::UrlSanitizer, lib: true do
expect(sanitizer.full_url).to eq('user@server:project.git') expect(sanitizer.full_url).to eq('user@server:project.git')
end end
end end
describe '.valid?' do
it 'validates url strings' do
expect(described_class.valid?(nil)).to be(false)
expect(described_class.valid?('valid@project:url.git')).to be(true)
expect(described_class.valid?('123://invalid:url')).to be(false)
end
end
end end
...@@ -197,6 +197,24 @@ describe Ci::Pipeline, models: true do ...@@ -197,6 +197,24 @@ describe Ci::Pipeline, models: true do
end end
end 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 end
describe '#stages_count' do describe '#stages_count' do
...@@ -647,7 +665,7 @@ describe Ci::Pipeline, models: true do ...@@ -647,7 +665,7 @@ describe Ci::Pipeline, models: true do
let(:pipeline) { create(:ci_pipeline, status: :manual) } let(:pipeline) { create(:ci_pipeline, status: :manual) }
it 'returns detailed status for blocked pipeline' do it 'returns detailed status for blocked pipeline' do
expect(subject.text).to eq 'manual' expect(subject.text).to eq 'blocked'
end end
end end
......
...@@ -170,22 +170,31 @@ describe Ci::Stage, models: true do ...@@ -170,22 +170,31 @@ describe Ci::Stage, models: true do
context 'when stage has warnings' do context 'when stage has warnings' do
context 'when using memoized warnings flag' do context 'when using memoized warnings flag' do
context 'when there are warnings' 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).not_to receive(:statuses)
expect(stage).to have_warnings expect(stage).to have_warnings
end end
end end
context 'when there are no warnings' do 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 receive(:statuses)
expect(stage).not_to have_warnings expect(stage).not_to have_warnings
end end
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 end
context 'when calculating warnings from statuses' do context 'when calculating warnings from statuses' do
......
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
...@@ -243,8 +243,8 @@ describe API::Milestones, api: true do ...@@ -243,8 +243,8 @@ describe API::Milestones, api: true do
describe 'confidential issues' do describe 'confidential issues' do
let(:public_project) { create(:empty_project, :public) } let(:public_project) { create(:empty_project, :public) }
let(:milestone) { create(:milestone, project: public_project) } let(:milestone) { create(:milestone, project: public_project) }
let(:issue) { create(:issue, project: public_project) } let(:issue) { create(:issue, project: public_project, position: 2) }
let(:confidential_issue) { create(:issue, confidential: true, project: public_project) } let(:confidential_issue) { create(:issue, confidential: true, project: public_project, position: 1) }
before do before do
public_project.team << [user, :developer] public_project.team << [user, :developer]
...@@ -283,11 +283,24 @@ describe API::Milestones, api: true do ...@@ -283,11 +283,24 @@ describe API::Milestones, api: true do
expect(json_response.size).to eq(1) expect(json_response.size).to eq(1)
expect(json_response.map { |issue| issue['id'] }).to include(issue.id) expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
end 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
end end
describe 'GET /projects/:id/milestones/:milestone_id/merge_requests' do 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 before do
milestone.merge_requests << merge_request milestone.merge_requests << merge_request
end end
...@@ -320,5 +333,18 @@ describe API::Milestones, api: true do ...@@ -320,5 +333,18 @@ describe API::Milestones, api: true do
expect(response).to have_http_status(401) expect(response).to have_http_status(401)
end 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
end end
...@@ -424,6 +424,14 @@ describe API::Projects, api: true do ...@@ -424,6 +424,14 @@ describe API::Projects, api: true do
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
end end
it 'ignores import_url when it is nil' do
project = attributes_for(:project, { import_url: nil })
post api('/projects', user), project
expect(response).to have_http_status(201)
end
context 'when a visibility level is restricted' do context 'when a visibility level is restricted' do
let(:project_param) { attributes_for(:project, visibility: 'public') } let(:project_param) { attributes_for(:project, visibility: 'public') }
......
require 'spec_helper' require 'spec_helper'
describe AuthorizedProjectsWorker do describe AuthorizedProjectsWorker do
let(:worker) { described_class.new } let(:project) { create(:empty_project) }
describe '.bulk_perform_and_wait' do describe '.bulk_perform_and_wait' do
it 'schedules the ids and waits for the jobs to complete' do it 'schedules the ids and waits for the jobs to complete' do
project = create(:project)
project.owner.project_authorizations.delete_all project.owner.project_authorizations.delete_all
described_class.bulk_perform_and_wait([[project.owner.id]]) described_class.bulk_perform_and_wait([[project.owner.id]])
...@@ -15,20 +13,37 @@ describe AuthorizedProjectsWorker do ...@@ -15,20 +13,37 @@ describe AuthorizedProjectsWorker do
end end
end end
describe '.bulk_perform_async' do
it "uses it's respective sidekiq queue" do
args = [[project.owner.id]]
push_bulk_args = {
'class' => described_class,
'queue' => described_class.sidekiq_options['queue'],
'args' => args
}
expect(Sidekiq::Client).to receive(:push_bulk).with(push_bulk_args).once
described_class.bulk_perform_async(args)
end
end
describe '#perform' do describe '#perform' do
subject { described_class.new }
it "refreshes user's authorized projects" do it "refreshes user's authorized projects" do
user = create(:user) user = create(:user)
expect_any_instance_of(User).to receive(:refresh_authorized_projects) expect_any_instance_of(User).to receive(:refresh_authorized_projects)
worker.perform(user.id) subject.perform(user.id)
end end
context "when the user is not found" do context "when the user is not found" do
it "does nothing" do it "does nothing" do
expect_any_instance_of(User).not_to receive(:refresh_authorized_projects) expect_any_instance_of(User).not_to receive(:refresh_authorized_projects)
described_class.new.perform(-1) subject.perform(-1)
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment