Commit 421acc9a authored by Clement Ho's avatar Clement Ho

Merge branch 'master' into backport-add-epic-sidebar

parents 74b87f02 2932f532
...@@ -416,12 +416,8 @@ ee_compat_check: ...@@ -416,12 +416,8 @@ ee_compat_check:
- /^[\d-]+-stable(-ee)?/ - /^[\d-]+-stable(-ee)?/
- branches@gitlab-org/gitlab-ee - branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee - branches@gitlab/gitlab-ee
allow_failure: yes allow_failure: no
retry: 0 retry: 0
cache:
key: "ee_compat_check_repo"
paths:
- ee_compat_check/ee-repo/
artifacts: artifacts:
name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}" name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}"
when: on_failure when: on_failure
......
...@@ -2,6 +2,22 @@ ...@@ -2,6 +2,22 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.1.3 (2017-11-10)
- [SECURITY] Prevent OAuth phishing attack by presenting detailed wording about app to user during authorization.
- [FIXED] Fix cancel button not working while uploading on the new issue page. !15137
- [FIXED] Fix webhooks recent deliveries. !15146 (Alexander Randa (@randaalex))
- [FIXED] Fix issues with forked projects of which the source was deleted. !15150
- [FIXED] Fix GPG signature popup info in Safari and Firefox. !15228
- [FIXED] Make sure group and project creation is blocked for new users that are external by default.
- [FIXED] Fix arguments Import/Export error importing project merge requests.
- [FIXED] Fix diff parser so it tolerates to diff special markers in the content.
- [FIXED] Fix a migration that adds merge_requests_ff_only_enabled column to MR table.
- [FIXED] Render 404 when polling commit notes without having permissions.
- [FIXED] Show error message when fast-forward merge is not possible.
- [FIXED] Avoid regenerating the ref path for the environment.
- [PERFORMANCE] Remove Filesystem check metrics that use too much CPU to handle requests.
## 10.1.2 (2017-11-08) ## 10.1.2 (2017-11-08)
- [SECURITY] Add X-Content-Type-Options header in API responses to make it more difficult to find other vulnerabilities. - [SECURITY] Add X-Content-Type-Options header in API responses to make it more difficult to find other vulnerabilities.
......
import { truncate } from './lib/utils/text_utility';
const MAX_MESSAGE_LENGTH = 500; const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message'; const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
...@@ -15,7 +17,7 @@ export default class AbuseReports { ...@@ -15,7 +17,7 @@ export default class AbuseReports {
if (reportMessage.length > MAX_MESSAGE_LENGTH) { if (reportMessage.length > MAX_MESSAGE_LENGTH) {
$messageCellElement.data('original-message', reportMessage); $messageCellElement.data('original-message', reportMessage);
$messageCellElement.data('message-truncated', 'true'); $messageCellElement.data('message-truncated', 'true');
$messageCellElement.text(window.gl.text.truncate(reportMessage, MAX_MESSAGE_LENGTH)); $messageCellElement.text(truncate(reportMessage, MAX_MESSAGE_LENGTH));
} }
} }
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import Vue from 'vue'; import Vue from 'vue';
import Flash from '../../../flash'; import Flash from '../../../flash';
import './lists_dropdown'; import './lists_dropdown';
import { pluralize } from '../../../lib/utils/text_utility';
const ModalStore = gl.issueBoards.ModalStore; const ModalStore = gl.issueBoards.ModalStore;
...@@ -21,7 +22,7 @@ gl.issueBoards.ModalFooter = Vue.extend({ ...@@ -21,7 +22,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
submitText() { submitText() {
const count = ModalStore.selectedCount(); const count = ModalStore.selectedCount();
return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`; return `Add ${count > 0 ? count : ''} ${pluralize('issue', count)}`;
}, },
}, },
methods: { methods: {
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
prefer-template, object-shorthand, prefer-arrow-callback */ prefer-template, object-shorthand, prefer-arrow-callback */
/* global Pager */ /* global Pager */
import { pluralize } from './lib/utils/text_utility';
export default (function () { export default (function () {
const CommitsList = {}; const CommitsList = {};
...@@ -86,7 +88,7 @@ export default (function () { ...@@ -86,7 +88,7 @@ export default (function () {
// Update commits count in the previous commits header. // Update commits count in the previous commits header.
commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length); commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length);
$commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${gl.text.pluralize('commit', commitsCount)}`); $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${pluralize('commit', commitsCount)}`);
} }
gl.utils.localTimeAgo($processedData.find('.js-timeago')); gl.utils.localTimeAgo($processedData.find('.js-timeago'));
......
/* eslint-disable func-names, prefer-arrow-callback */ /* eslint-disable func-names, prefer-arrow-callback */
import Api from './api'; import Api from './api';
import { humanize } from './lib/utils/text_utility';
export default class CreateLabelDropdown { export default class CreateLabelDropdown {
constructor($el, namespacePath, projectPath) { constructor($el, namespacePath, projectPath) {
...@@ -107,7 +108,7 @@ export default class CreateLabelDropdown { ...@@ -107,7 +108,7 @@ export default class CreateLabelDropdown {
errors = label.message; errors = label.message;
} else { } else {
errors = Object.keys(label.message).map(key => errors = Object.keys(label.message).map(key =>
`${gl.text.humanize(key)} ${label.message[key].join(', ')}`, `${humanize(key)} ${label.message[key].join(', ')}`,
).join('<br/>'); ).join('<br/>');
} }
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { __ } from '../locale'; import { __ } from '../locale';
import '../lib/utils/text_utility'; import { dasherize } from '../lib/utils/text_utility';
import DEFAULT_EVENT_OBJECTS from './default_event_objects'; import DEFAULT_EVENT_OBJECTS from './default_event_objects';
const EMPTY_STAGE_TEXTS = { const EMPTY_STAGE_TEXTS = {
...@@ -36,7 +36,7 @@ export default { ...@@ -36,7 +36,7 @@ export default {
}); });
newData.stages.forEach((item) => { newData.stages.forEach((item) => {
const stageSlug = gl.text.dasherize(item.name.toLowerCase()); const stageSlug = dasherize(item.name.toLowerCase());
item.active = false; item.active = false;
item.isUserAllowed = data.permissions[stageSlug]; item.isUserAllowed = data.permissions[stageSlug];
item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug]; item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import Timeago from 'timeago.js'; import Timeago from 'timeago.js';
import _ from 'underscore'; import _ from 'underscore';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import '../../lib/utils/text_utility'; import { humanize } from '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue'; import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue'; import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue'; import StopComponent from './environment_stop.vue';
...@@ -139,7 +139,7 @@ export default { ...@@ -139,7 +139,7 @@ export default {
if (this.hasManualActions) { if (this.hasManualActions) {
return this.model.last_deployment.manual_actions.map((action) => { return this.model.last_deployment.manual_actions.map((action) => {
const parsedAction = { const parsedAction = {
name: gl.text.humanize(action.name), name: humanize(action.name),
play_path: action.play_path, play_path: action.play_path,
playable: action.playable, playable: action.playable,
}; };
......
...@@ -338,7 +338,8 @@ class GfmAutoComplete { ...@@ -338,7 +338,8 @@ class GfmAutoComplete {
let resultantValue = value; let resultantValue = value;
if (value && !this.setting.skipSpecialCharacterTest) { if (value && !this.setting.skipSpecialCharacterTest) {
const withoutAt = value.substring(1); const withoutAt = value.substring(1);
if (withoutAt && /[^\w\d]/.test(withoutAt)) { const regex = value.charAt() === '~' ? /\W|^\d+$/ : /\W/;
if (withoutAt && regex.test(withoutAt)) {
resultantValue = `${value.charAt()}"${withoutAt}"`; resultantValue = `${value.charAt()}"${withoutAt}"`;
} }
} }
......
...@@ -331,7 +331,7 @@ GitLabDropdown = (function() { ...@@ -331,7 +331,7 @@ GitLabDropdown = (function() {
if (_this.dropdown.find('.dropdown-toggle-page').length) { if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector; selector = ".dropdown-page-one " + selector;
} }
return $(selector); return $(selector, this.instance.dropdown);
}; };
})(this), })(this),
data: (function(_this) { data: (function(_this) {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import GfmAutoComplete from './gfm_auto_complete'; import GfmAutoComplete from './gfm_auto_complete';
import dropzoneInput from './dropzone_input'; import dropzoneInput from './dropzone_input';
import textUtils from './lib/utils/text_markdown';
export default class GLForm { export default class GLForm {
constructor(form, enableGFM = false) { constructor(form, enableGFM = false) {
...@@ -46,7 +47,7 @@ export default class GLForm { ...@@ -46,7 +47,7 @@ export default class GLForm {
} }
// form and textarea event listeners // form and textarea event listeners
this.addEventListeners(); this.addEventListeners();
gl.text.init(this.form); textUtils.init(this.form);
// hide discard button // hide discard button
this.form.find('.js-note-discard').hide(); this.form.find('.js-note-discard').hide();
this.form.show(); this.form.show();
...@@ -85,7 +86,7 @@ export default class GLForm { ...@@ -85,7 +86,7 @@ export default class GLForm {
clearEventListeners() { clearEventListeners() {
this.textarea.off('focus'); this.textarea.off('focus');
this.textarea.off('blur'); this.textarea.off('blur');
gl.text.removeListeners(this.form); textUtils.removeListeners(this.form);
} }
addEventListeners() { addEventListeners() {
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
import 'vendor/jquery.waitforimages'; import 'vendor/jquery.waitforimages';
import '~/lib/utils/text_utility'; import { addDelimiter } from './lib/utils/text_utility';
import Flash from './flash'; import Flash from './flash';
import TaskList from './task_list'; import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown'; import CreateMergeRequestDropdown from './create_merge_request_dropdown';
...@@ -73,7 +73,7 @@ export default class Issue { ...@@ -73,7 +73,7 @@ export default class Issue {
let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, '')); let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1; numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues)); projectIssuesCounter.text(addDelimiter(numProjectIssues));
if (this.createMergeRequestDropdown) { if (this.createMergeRequestDropdown) {
if (isClosed) { if (isClosed) {
......
...@@ -172,7 +172,6 @@ export const getSelectedFragment = () => { ...@@ -172,7 +172,6 @@ export const getSelectedFragment = () => {
return documentFragment; return documentFragment;
}; };
// TODO: Update this name, there is a gl.text.insertText function.
export const insertText = (target, text) => { export const insertText = (target, text) => {
// Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
const selectionStart = target.selectionStart; const selectionStart = target.selectionStart;
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import timeago from 'timeago.js'; import timeago from 'timeago.js';
import dateFormat from 'vendor/date.format'; import dateFormat from 'vendor/date.format';
import { pluralize } from './text_utility';
import { import {
lang, lang,
...@@ -142,9 +143,9 @@ export function timeIntervalInWords(intervalInSeconds) { ...@@ -142,9 +143,9 @@ export function timeIntervalInWords(intervalInSeconds) {
let text = ''; let text = '';
if (minutes >= 1) { if (minutes >= 1) {
text = `${minutes} ${gl.text.pluralize('minute', minutes)} ${seconds} ${gl.text.pluralize('second', seconds)}`; text = `${minutes} ${pluralize('minute', minutes)} ${seconds} ${pluralize('second', seconds)}`;
} else { } else {
text = `${seconds} ${gl.text.pluralize('second', seconds)}`; text = `${seconds} ${pluralize('second', seconds)}`;
} }
return text; return text;
} }
......
/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
const textUtils = {};
textUtils.selectedText = function(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
};
textUtils.lineBefore = function(text, textarea) {
var split;
split = text.substring(0, textarea.selectionStart).trim().split('\n');
return split[split.length - 1];
};
textUtils.lineAfter = function(text, textarea) {
return text.substring(textarea.selectionEnd).trim().split('\n')[0];
};
textUtils.blockTagText = function(text, textArea, blockTag, selected) {
var lineAfter, lineBefore;
lineBefore = this.lineBefore(text, textArea);
lineAfter = this.lineAfter(text, textArea);
if (lineBefore === blockTag && lineAfter === blockTag) {
// To remove the block tag we have to select the line before & after
if (blockTag != null) {
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
}
return selected;
} else {
return blockTag + "\n" + selected + "\n" + blockTag;
}
};
textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
removedLastNewLine = false;
removedFirstNewLine = false;
currentLineEmpty = false;
// Remove the first newline
if (selected.indexOf('\n') === 0) {
removedFirstNewLine = true;
selected = selected.replace(/\n+/, '');
}
// Remove the last newline
if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
removedLastNewLine = true;
selected = selected.replace(/\n$/, '');
}
selectedSplit = selected.split('\n');
if (!wrap) {
lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
// Check whether the current line is empty or consists only of spaces(=handle as empty)
if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
currentLineEmpty = true;
}
}
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
if (blockTag != null && blockTag !== '') {
insertText = this.blockTagText(text, textArea, blockTag, selected);
} else {
insertText = selectedSplit.map(function(val) {
if (val.indexOf(tag) === 0) {
return "" + (val.replace(tag, ''));
} else {
return "" + tag + val;
}
}).join('\n');
}
} else {
insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
}
if (removedFirstNewLine) {
insertText = '\n' + insertText;
}
if (removedLastNewLine) {
insertText += '\n';
}
if (document.queryCommandSupported('insertText')) {
inserted = document.execCommand('insertText', false, insertText);
}
if (!inserted) {
try {
document.execCommand("ms-beginUndoUnit");
} catch (error) {}
textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
try {
document.execCommand("ms-endUndoUnit");
} catch (error) {}
}
return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
};
textUtils.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
var pos;
if (!textArea.setSelectionRange) {
return;
}
if (textArea.selectionStart === textArea.selectionEnd) {
if (wrapped) {
pos = textArea.selectionStart - tag.length;
} else {
pos = textArea.selectionStart;
}
if (removedLastNewLine) {
pos -= 1;
}
return textArea.setSelectionRange(pos, pos);
}
};
textUtils.updateText = function(textArea, tag, blockTag, wrap) {
var $textArea, selected, text;
$textArea = $(textArea);
textArea = $textArea.get(0);
text = $textArea.val();
selected = this.selectedText(text, textArea);
$textArea.focus();
return this.insertText(textArea, text, tag, blockTag, selected, wrap);
};
textUtils.init = function(form) {
var self;
self = this;
return $('.js-md', form).off('click').on('click', function() {
var $this;
$this = $(this);
return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
});
};
textUtils.removeListeners = function(form) {
return $('.js-md', form).off('click');
};
textUtils.replaceRange = function(s, start, end, substitute) {
return s.substring(0, start) + substitute + s.substring(end);
};
export default textUtils;
/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */ /**
* Adds a , to a string composed by numbers, at every 3 chars.
import 'vendor/latinise'; *
* 2333 -> 2,333
var base; * 232324 -> 232,324
var w = window; *
if (w.gl == null) { * @param {String} text
w.gl = {}; * @returns {String}
} */
if ((base = w.gl).text == null) { export const addDelimiter = text => (text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text);
base.text = {};
}
gl.text.addDelimiter = function(text) {
return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
};
/** /**
* Returns '99+' for numbers bigger than 99. * Returns '99+' for numbers bigger than 99.
...@@ -20,182 +15,53 @@ gl.text.addDelimiter = function(text) { ...@@ -20,182 +15,53 @@ gl.text.addDelimiter = function(text) {
* @param {Number} count * @param {Number} count
* @return {Number|String} * @return {Number|String}
*/ */
export function highCountTrim(count) { export const highCountTrim = count => (count > 99 ? '99+' : count);
return count > 99 ? '99+' : count;
}
/**
* Capitalizes first character
*
* @param {String} text
* @return {String}
*/
export function capitalizeFirstCharacter(text) { export function capitalizeFirstCharacter(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`; return `${text[0].toUpperCase()}${text.slice(1)}`;
} }
gl.text.randomString = function() { /**
return Math.random().toString(36).substring(7); * Converst first char to uppercase and replaces undercores with spaces
}; * @param {String} string
gl.text.replaceRange = function(s, start, end, substitute) { * @requires {String}
return s.substring(0, start) + substitute + s.substring(end); */
}; export const humanize = string => string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
gl.text.getTextWidth = function(text, font) {
/**
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
*
* @param {String} text The text to be rendered.
* @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
*
* @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
*/
// re-use canvas object for better performance
var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
var context = canvas.getContext('2d');
context.font = font;
return context.measureText(text).width;
};
gl.text.selectedText = function(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
};
gl.text.lineBefore = function(text, textarea) {
var split;
split = text.substring(0, textarea.selectionStart).trim().split('\n');
return split[split.length - 1];
};
gl.text.lineAfter = function(text, textarea) {
return text.substring(textarea.selectionEnd).trim().split('\n')[0];
};
gl.text.blockTagText = function(text, textArea, blockTag, selected) {
var lineAfter, lineBefore;
lineBefore = this.lineBefore(text, textArea);
lineAfter = this.lineAfter(text, textArea);
if (lineBefore === blockTag && lineAfter === blockTag) {
// To remove the block tag we have to select the line before & after
if (blockTag != null) {
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
}
return selected;
} else {
return blockTag + "\n" + selected + "\n" + blockTag;
}
};
gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
removedLastNewLine = false;
removedFirstNewLine = false;
currentLineEmpty = false;
// Remove the first newline
if (selected.indexOf('\n') === 0) {
removedFirstNewLine = true;
selected = selected.replace(/\n+/, '');
}
// Remove the last newline
if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
removedLastNewLine = true;
selected = selected.replace(/\n$/, '');
}
selectedSplit = selected.split('\n');
if (!wrap) {
lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
// Check whether the current line is empty or consists only of spaces(=handle as empty)
if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
currentLineEmpty = true;
}
}
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
if (blockTag != null && blockTag !== '') {
insertText = this.blockTagText(text, textArea, blockTag, selected);
} else {
insertText = selectedSplit.map(function(val) {
if (val.indexOf(tag) === 0) {
return "" + (val.replace(tag, ''));
} else {
return "" + tag + val;
}
}).join('\n');
}
} else {
insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
}
if (removedFirstNewLine) { /**
insertText = '\n' + insertText; * Adds an 's' to the end of the string when count is bigger than 0
} * @param {String} str
* @param {Number} count
* @returns {String}
*/
export const pluralize = (str, count) => str + (count > 1 || count === 0 ? 's' : '');
if (removedLastNewLine) { /**
insertText += '\n'; * Replaces underscores with dashes
} * @param {*} str
* @returns {String}
*/
export const dasherize = str => str.replace(/[_\s]+/g, '-');
if (document.queryCommandSupported('insertText')) { /**
inserted = document.execCommand('insertText', false, insertText); * Removes accents and converts to lower case
} * @param {String} str
if (!inserted) { * @returns {String}
try { */
document.execCommand("ms-beginUndoUnit"); export const slugify = str => str.trim().toLowerCase();
} catch (error) {}
textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
try {
document.execCommand("ms-endUndoUnit");
} catch (error) {}
}
return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
};
gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
var pos;
if (!textArea.setSelectionRange) {
return;
}
if (textArea.selectionStart === textArea.selectionEnd) {
if (wrapped) {
pos = textArea.selectionStart - tag.length;
} else {
pos = textArea.selectionStart;
}
if (removedLastNewLine) { /**
pos -= 1; * Truncates given text
} *
* @param {String} string
* @param {Number} maxLength
* @returns {String}
*/
export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`;
return textArea.setSelectionRange(pos, pos);
}
};
gl.text.updateText = function(textArea, tag, blockTag, wrap) {
var $textArea, selected, text;
$textArea = $(textArea);
textArea = $textArea.get(0);
text = $textArea.val();
selected = this.selectedText(text, textArea);
$textArea.focus();
return this.insertText(textArea, text, tag, blockTag, selected, wrap);
};
gl.text.init = function(form) {
var self;
self = this;
return $('.js-md', form).off('click').on('click', function() {
var $this;
$this = $(this);
return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
});
};
gl.text.removeListeners = function(form) {
return $('.js-md', form).off('click');
};
gl.text.humanize = function(string) {
return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
};
gl.text.pluralize = function(str, count) {
return str + (count > 1 || count === 0 ? 's' : '');
};
gl.text.truncate = function(string, maxLength) {
return string.substr(0, (maxLength - 3)) + '...';
};
gl.text.dasherize = function(str) {
return str.replace(/[_\s]+/g, '-');
};
gl.text.slugify = function(str) {
return str.trim().toLowerCase().latinise();
};
...@@ -30,7 +30,6 @@ import './commit/image_file'; ...@@ -30,7 +30,6 @@ import './commit/image_file';
import { handleLocationHash } from './lib/utils/common_utils'; import { handleLocationHash } from './lib/utils/common_utils';
import './lib/utils/datetime_utility'; import './lib/utils/datetime_utility';
import './lib/utils/pretty_time'; import './lib/utils/pretty_time';
import './lib/utils/text_utility';
import './lib/utils/url_utility'; import './lib/utils/url_utility';
// behaviors // behaviors
......
...@@ -5,6 +5,7 @@ import 'vendor/jquery.waitforimages'; ...@@ -5,6 +5,7 @@ import 'vendor/jquery.waitforimages';
import TaskList from './task_list'; import TaskList from './task_list';
import './merge_request_tabs'; import './merge_request_tabs';
import IssuablesHelper from './helpers/issuables_helper'; import IssuablesHelper from './helpers/issuables_helper';
import { addDelimiter } from './lib/utils/text_utility';
(function() { (function() {
this.MergeRequest = (function() { this.MergeRequest = (function() {
...@@ -124,7 +125,7 @@ import IssuablesHelper from './helpers/issuables_helper'; ...@@ -124,7 +125,7 @@ import IssuablesHelper from './helpers/issuables_helper';
const $el = $('.nav-links .js-merge-counter'); const $el = $('.nav-links .js-merge-counter');
const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0); const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0);
$el.text(gl.text.addDelimiter(count)); $el.text(addDelimiter(count));
}; };
MergeRequest.prototype.hideCloseButton = function() { MergeRequest.prototype.hideCloseButton = function() {
......
<script> <script>
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
import icon from '../../../vue_shared/components/icon.vue'; import icon from '../../../vue_shared/components/icon.vue';
import { dasherize } from '../../../lib/utils/text_utility';
/** /**
* Renders either a cancel, retry or play icon pointing to the given path. * Renders either a cancel, retry or play icon pointing to the given path.
* TODO: Remove UJS from here and use an async request instead. * TODO: Remove UJS from here and use an async request instead.
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
computed: { computed: {
cssClass() { cssClass() {
const actionIconDash = gl.text.dasherize(this.actionIcon); const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`; return `${actionIconDash} js-icon-${actionIconDash}`;
}, },
}, },
......
...@@ -3,7 +3,7 @@ import flash from '../../flash'; ...@@ -3,7 +3,7 @@ import flash from '../../flash';
import service from '../services'; import service from '../services';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const redirectToUrl = url => gl.utils.visitUrl(url); export const redirectToUrl = (_, url) => gl.utils.visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
...@@ -84,7 +84,7 @@ export const commitChanges = ({ commit, state, dispatch, getters }, { payload, n ...@@ -84,7 +84,7 @@ export const commitChanges = ({ commit, state, dispatch, getters }, { payload, n
flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice'); flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
if (newMr) { if (newMr) {
redirectToUrl(`${state.endpoints.newMergeRequestUrl}${branch}`); dispatch('redirectToUrl', `${state.endpoints.newMergeRequestUrl}${branch}`);
} else { } else {
commit(types.SET_COMMIT_REF, data.id); commit(types.SET_COMMIT_REF, data.id);
......
...@@ -3,16 +3,16 @@ import * as types from '../mutation_types'; ...@@ -3,16 +3,16 @@ import * as types from '../mutation_types';
import { pushState } from '../utils'; import { pushState } from '../utils';
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export const createNewBranch = ({ rootState, commit }, branch) => service.createBranch( export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
rootState.project.id, state.project.id,
{ {
branch, branch,
ref: rootState.currentBranch, ref: state.currentBranch,
}, },
).then(res => res.json()) ).then(res => res.json())
.then((data) => { .then((data) => {
const branchName = data.name; const branchName = data.name;
const url = location.href.replace(rootState.currentBranch, branchName); const url = location.href.replace(state.currentBranch, branchName);
pushState(url); pushState(url);
......
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import '../../lib/utils/text_utility'; import { pluralize } from '../../lib/utils/text_utility';
export default { export default {
name: 'MRWidgetHeader', name: 'MRWidgetHeader',
...@@ -14,7 +14,7 @@ export default { ...@@ -14,7 +14,7 @@ export default {
return this.mr.divergedCommitsCount > 0; return this.mr.divergedCommitsCount > 0;
}, },
commitsText() { commitsText() {
return gl.text.pluralize('commit', this.mr.divergedCommitsCount); return pluralize('commit', this.mr.divergedCommitsCount);
}, },
branchNameClipboardData() { branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that // This supports code in app/assets/javascripts/copy_to_clipboard.js that
......
import bp from './breakpoints'; import bp from './breakpoints';
import { slugify } from './lib/utils/text_utility';
export default class Wikis { export default class Wikis {
constructor() { constructor() {
...@@ -23,7 +24,7 @@ export default class Wikis { ...@@ -23,7 +24,7 @@ export default class Wikis {
if (!this.newWikiForm) return; if (!this.newWikiForm) return;
const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
const slug = gl.text.slugify(slugInput.value); const slug = slugify(slugInput.value);
if (slug.length > 0) { if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path'); const wikisPath = slugInput.getAttribute('data-wikis-path');
......
...@@ -45,7 +45,7 @@ class AutocompleteUsersFinder ...@@ -45,7 +45,7 @@ class AutocompleteUsersFinder
def find_users def find_users
return users_from_project if project return users_from_project if project
return group.users if group return group.users_with_parents if group
return User.all if current_user return User.all if current_user
User.none User.none
......
...@@ -86,6 +86,14 @@ module Milestoneish ...@@ -86,6 +86,14 @@ module Milestoneish
false false
end end
def total_issue_time_spent
@total_issue_time_spent ||= issues.joins(:timelogs).sum(:time_spent)
end
def human_total_issue_time_spent
Gitlab::TimeTrackingFormatter.output(total_issue_time_spent)
end
private private
def count_issues_by_state(user) def count_issues_by_state(user)
......
...@@ -262,10 +262,6 @@ class Issue < ActiveRecord::Base ...@@ -262,10 +262,6 @@ class Issue < ActiveRecord::Base
true true
end end
def update_project_counter_caches?
state_changed? || confidential_changed?
end
def update_project_counter_caches def update_project_counter_caches
Projects::OpenIssuesCountService.new(project).refresh_cache Projects::OpenIssuesCountService.new(project).refresh_cache
end end
......
...@@ -958,10 +958,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -958,10 +958,6 @@ class MergeRequest < ActiveRecord::Base
true true
end end
def update_project_counter_caches?
state_changed?
end
def update_project_counter_caches def update_project_counter_caches
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
end end
......
...@@ -69,6 +69,10 @@ class PagesDomain < ActiveRecord::Base ...@@ -69,6 +69,10 @@ class PagesDomain < ActiveRecord::Base
current < x509.not_before || x509.not_after < current current < x509.not_before || x509.not_after < current
end end
def expiration
x509&.not_after
end
def subject def subject
return unless x509 return unless x509
x509.subject.to_s x509.subject.to_s
......
module Ci module Ci
class PipelineTriggerService < BaseService class PipelineTriggerService < BaseService
include Gitlab::Utils::StrongMemoize
def execute def execute
if trigger_from_token if trigger_from_token
create_pipeline_from_trigger(trigger_from_token) create_pipeline_from_trigger(trigger_from_token)
...@@ -26,9 +28,9 @@ module Ci ...@@ -26,9 +28,9 @@ module Ci
end end
def trigger_from_token def trigger_from_token
return @trigger if defined?(@trigger) strong_memoize(:trigger) do
Ci::Trigger.find_by_token(params[:token].to_s)
@trigger = Ci::Trigger.find_by_token(params[:token].to_s) end
end end
def create_pipeline_variables!(pipeline) def create_pipeline_variables!(pipeline)
......
...@@ -187,7 +187,7 @@ class IssuableBaseService < BaseService ...@@ -187,7 +187,7 @@ class IssuableBaseService < BaseService
# We have to perform this check before saving the issuable as Rails resets # We have to perform this check before saving the issuable as Rails resets
# the changed fields upon calling #save. # the changed fields upon calling #save.
update_project_counters = issuable.project && issuable.update_project_counter_caches? update_project_counters = issuable.project && update_project_counter_caches?(issuable)
if issuable.with_transaction_returning_status { issuable.save } if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field # We do not touch as it will affect a update on updated_at field
...@@ -288,4 +288,8 @@ class IssuableBaseService < BaseService ...@@ -288,4 +288,8 @@ class IssuableBaseService < BaseService
# override if needed # override if needed
def execute_hooks(issuable, action = 'open', params = {}) def execute_hooks(issuable, action = 'open', params = {})
end end
def update_project_counter_caches?(issuable)
issuable.state_changed?
end
end end
...@@ -45,5 +45,9 @@ module Issues ...@@ -45,5 +45,9 @@ module Issues
params.delete(:assignee_ids) params.delete(:assignee_ids)
end end
end end
def update_project_counter_caches?(issue)
super || issue.confidential_changed?
end
end end
end end
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
%th Projects %th Projects
%th Jobs %th Jobs
%th Tags %th Tags
%th Last contact %th= link_to 'Last contact', admin_runners_path(params.slice(:search).merge(sort: 'contacted_asc'))
%th %th
- @runners.each do |runner| - @runners.each do |runner|
......
- auth_app_owner = @pre_auth.client.application.owner
%main{ :role => "main" } %main{ :role => "main" }
.modal-no-backdrop.modal-doorkeepr-auth .modal-no-backdrop.modal-doorkeepr-auth
.modal-content .modal-content
...@@ -20,9 +18,14 @@ ...@@ -20,9 +18,14 @@
%p %p
An application called An application called
= link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer' = link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer'
is requesting access to your GitLab account. This application was created by is requesting access to your GitLab account.
= succeed "." do
= link_to auth_app_owner.name, user_path(auth_app_owner) - auth_app_owner = @pre_auth.client.application.owner
- if auth_app_owner
This application was created by
= succeed "." do
= link_to auth_app_owner.name, user_path(auth_app_owner)
Please note that this application is not provided by GitLab and you should verify its authenticity before Please note that this application is not provided by GitLab and you should verify its authenticity before
allowing access. allowing access.
- if @pre_auth.scopes - if @pre_auth.scopes
......
- discussion = @note.discussion if @note.part_of_discussion? - discussion = @note.discussion if @note.part_of_discussion?
- diff_discussion = discussion&.diff_discussion?
- on_image = discussion.on_image? if diff_discussion
- if discussion - if discussion
- phrase_end_char = on_image ? "." : ":"
%p.details %p.details
= succeed ':' do = succeed phrase_end_char do
= link_to @note.author_name, user_url(@note.author) = link_to @note.author_name, user_url(@note.author)
- if discussion.diff_discussion? - if diff_discussion
- if discussion.new_discussion? - if discussion.new_discussion?
started a new discussion started a new discussion
- else - else
...@@ -21,7 +26,7 @@ ...@@ -21,7 +26,7 @@
%p.details %p.details
#{link_to @note.author_name, user_url(@note.author)} commented: #{link_to @note.author_name, user_url(@note.author)} commented:
- if discussion&.diff_discussion? - if diff_discussion && !on_image
= content_for :head do = content_for :head do
= stylesheet_link_tag 'mailers/highlighted_diff_email' = stylesheet_link_tag 'mailers/highlighted_diff_email'
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
.create_access_levels-container .create_access_levels-container
= dropdown_tag('Select', = dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-create wide', options: { toggle_class: 'js-allowed-to-create wide',
dropdown_class: 'dropdown-menu-selectable', dropdown_class: 'dropdown-menu-selectable capitalize-header',
data: { field_name: 'protected_tag[create_access_levels_attributes][0][access_level]', input_id: 'create_access_levels_attributes' }}) data: { field_name: 'protected_tag[create_access_levels_attributes][0][access_level]', input_id: 'create_access_levels_attributes' }})
= render 'projects/protected_tags/shared/create_protected_tag' = render 'projects/protected_tags/shared/create_protected_tag'
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></svg>
...@@ -85,6 +85,22 @@ ...@@ -85,6 +85,22 @@
Closed: Closed:
= milestone.issues_visible_to_user(current_user).closed.count = milestone.issues_visible_to_user(current_user).closed.count
.block.time_spent
.sidebar-collapsed-icon
= custom_icon('icon_hourglass')
%span.collapsed-milestone-total-time-spent
- if milestone.human_total_issue_time_spent
= milestone.human_total_issue_time_spent
- else
= _("None")
.title.hide-collapsed
= _("Total issue time spent")
.value.hide-collapsed
- if milestone.human_total_issue_time_spent
%span.bold= milestone.human_total_issue_time_spent
- else
%span.no-value= _("No time spent")
.block.merge-requests .block.merge-requests
.sidebar-collapsed-icon .sidebar-collapsed-icon
%strong %strong
......
---
title: Prevent OAuth phishing attack by presenting detailed wording about app to user
during authorization
merge_request:
author:
type: security
---
title: Fix errors when selecting numeric-only labels in the labels autocomplete selector
merge_request: 14607
author: haseebeqx
type: fixed
---
title: Fix GPG signature popup info in Safari and Firefox
merge_request: 15228
author:
type: fixed
---
title: Add total time spent to milestones
merge_request: 15116
author: George Andrinopoulos
type: added
---
title: Add administrative endpoint to list all pages domains
merge_request: 15160
author: Travis Miller
type: added
---
title: Move update_project_counter_caches? out of issue and merge request
merge_request: 15300
author: George Andrinopoulos
type: other
---
title: Fix webhooks recent deliveries
merge_request: 15146
author: Alexander Randa (@randaalex)
type: fixed
---
title: Revert a regression on runners sorting (!15134)
merge_request: 15341
author: Takuya Noguchi
type: fixed
---
title: Fix issues with forked projects of which the source was deleted
merge_request: 15150
author:
type: fixed
---
title: Prevent error when authorizing an admin-created OAauth application without
a set owner
merge_request:
author:
type: fixed
---
title: Make sure group and project creation is blocked for new users that are external
by default
merge_request:
author:
type: fixed
---
title: Fix arguments Import/Export error importing project merge requests
merge_request:
author:
type: fixed
--- ---
title: Avoid regenerating the ref path for the environment title: Fix user autocomplete in subgroups
merge_request: merge_request:
author: author:
type: fixed type: fixed
---
title: Fix diff parser so it tolerates to diff special markers in the content
merge_request:
author:
type: fixed
---
title: Fix a migration that adds merge_requests_ff_only_enabled column to MR table
merge_request:
author:
type: fixed
---
title: Render 404 when polling commit notes without having permissions
merge_request:
author:
type: fixed
--- ---
title: Show error message when fast-forward merge is not possible title: Fix image diff notification email from showing wrong content
merge_request: merge_request:
author: author:
type: fixed type: fixed
---
title: Fix cancel button not working while uploading on the new issue page
merge_request: 15137
author:
type: fixed
---
title: Remove Filesystem check metrics that use too much CPU to handle requests
merge_request:
author:
type: performance
---
title: Export text utils functions as es6 module and add tests
merge_request:
author:
type: other
...@@ -4,6 +4,31 @@ Endpoints for connecting custom domain(s) and TLS certificates in [GitLab Pages] ...@@ -4,6 +4,31 @@ Endpoints for connecting custom domain(s) and TLS certificates in [GitLab Pages]
The GitLab Pages feature must be enabled to use these endpoints. Find out more about [administering](../administration/pages/index.md) and [using](../user/project/pages/index.md) the feature. The GitLab Pages feature must be enabled to use these endpoints. Find out more about [administering](../administration/pages/index.md) and [using](../user/project/pages/index.md) the feature.
## List all pages domains
Get a list of all pages domains. The user must have admin permissions.
```http
GET /pages/domains
```
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/pages/domains
```
```json
[
{
"domain": "ssl.domain.example",
"url": "https://ssl.domain.example",
"certificate": {
"expired": false,
"expiration": "2020-04-12T14:32:00.000Z"
}
}
]
```
## List pages domains ## List pages domains
Get a list of project pages domains. The user must have permissions to view pages domains. Get a list of project pages domains. The user must have permissions to view pages domains.
......
...@@ -490,6 +490,41 @@ Remove all previously JIRA settings from a project. ...@@ -490,6 +490,41 @@ Remove all previously JIRA settings from a project.
DELETE /projects/:id/services/jira DELETE /projects/:id/services/jira
``` ```
## Kubernetes
Kubernetes / Openshift integration
### Create/Edit Kubernetes service
Set Kubernetes service for a project.
```
PUT /projects/:id/services/kubernetes
```
Parameters:
- `namespace` (**required**) - The Kubernetes namespace to use
- `api_url` (**required**) - The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com
- `token` (**required**) - The service token to authenticate against the Kubernetes cluster with
- `ca_pem` (optional) - A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)
### Delete Kubernetes service
Delete Kubernetes service for a project.
```
DELETE /projects/:id/services/kubernetes
```
### Get Kubernetes service settings
Get Kubernetes service settings for a project.
```
GET /projects/:id/services/kubernetes
```
## Slack slash commands ## Slack slash commands
Ability to receive slash commands from a Slack chat instance. Ability to receive slash commands from a Slack chat instance.
......
...@@ -21,7 +21,7 @@ want to add. ...@@ -21,7 +21,7 @@ want to add.
--- ---
Select the user and the [permission level](../../user/permissions.md) Select the user and the [permission level](../../permissions.md)
that you'd like to give the user. Note that you can select more than one user. that you'd like to give the user. Note that you can select more than one user.
![Give user permissions](img/add_user_give_permissions.png) ![Give user permissions](img/add_user_give_permissions.png)
......
...@@ -42,6 +42,8 @@ module API ...@@ -42,6 +42,8 @@ module API
# Helper Methods for Grape Endpoint # Helper Methods for Grape Endpoint
module HelperMethods module HelperMethods
include Gitlab::Utils::StrongMemoize
def find_current_user! def find_current_user!
user = find_user_from_access_token || find_user_from_warden user = find_user_from_access_token || find_user_from_warden
return unless user return unless user
...@@ -52,9 +54,9 @@ module API ...@@ -52,9 +54,9 @@ module API
end end
def access_token def access_token
return @access_token if defined?(@access_token) strong_memoize(:access_token) do
find_oauth_access_token || find_personal_access_token
@access_token = find_oauth_access_token || find_personal_access_token end
end end
def validate_access_token!(scopes: []) def validate_access_token!(scopes: [])
......
...@@ -1042,6 +1042,11 @@ module API ...@@ -1042,6 +1042,11 @@ module API
expose :value expose :value
end end
class PagesDomainCertificateExpiration < Grape::Entity
expose :expired?, as: :expired
expose :expiration
end
class PagesDomainCertificate < Grape::Entity class PagesDomainCertificate < Grape::Entity
expose :subject expose :subject
expose :expired?, as: :expired expose :expired?, as: :expired
...@@ -1049,12 +1054,23 @@ module API ...@@ -1049,12 +1054,23 @@ module API
expose :certificate_text expose :certificate_text
end end
class PagesDomainBasic < Grape::Entity
expose :domain
expose :url
expose :certificate,
as: :certificate_expiration,
if: ->(pages_domain, _) { pages_domain.certificate? },
using: PagesDomainCertificateExpiration do |pages_domain|
pages_domain
end
end
class PagesDomain < Grape::Entity class PagesDomain < Grape::Entity
expose :domain expose :domain
expose :url expose :url
expose :certificate, expose :certificate,
if: ->(pages_domain, _) { pages_domain.certificate? }, if: ->(pages_domain, _) { pages_domain.certificate? },
using: PagesDomainCertificate do |pages_domain| using: PagesDomainCertificate do |pages_domain|
pages_domain pages_domain
end end
end end
......
...@@ -155,6 +155,11 @@ module API ...@@ -155,6 +155,11 @@ module API
end end
end end
def authenticated_with_full_private_access!
authenticate!
forbidden! unless current_user.full_private_access?
end
def authenticated_as_admin! def authenticated_as_admin!
authenticate! authenticate!
forbidden! unless current_user.admin? forbidden! unless current_user.admin?
...@@ -190,6 +195,10 @@ module API ...@@ -190,6 +195,10 @@ module API
not_found! unless user_project.pages_available? not_found! unless user_project.pages_available?
end end
def require_pages_config_enabled!
not_found! unless Gitlab.config.pages.enabled
end
def can?(object, action, subject = :global) def can?(object, action, subject = :global)
Ability.allowed?(object, action, subject) Ability.allowed?(object, action, subject)
end end
......
...@@ -4,7 +4,6 @@ module API ...@@ -4,7 +4,6 @@ module API
before do before do
authenticate! authenticate!
require_pages_enabled!
end end
after_validation do after_validation do
...@@ -29,10 +28,31 @@ module API ...@@ -29,10 +28,31 @@ module API
end end
end end
resource :pages do
before do
require_pages_config_enabled!
authenticated_with_full_private_access!
end
desc "Get all pages domains" do
success Entities::PagesDomainBasic
end
params do
use :pagination
end
get "domains" do
present paginate(PagesDomain.all), with: Entities::PagesDomainBasic
end
end
params do params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'
end end
resource :projects, requirements: { id: %r{[^/]+} } do resource :projects, requirements: { id: %r{[^/]+} } do
before do
require_pages_enabled!
end
desc 'Get all pages domains' do desc 'Get all pages domains' do
success Entities::PagesDomain success Entities::PagesDomain
end end
......
...@@ -104,7 +104,7 @@ module Gitlab ...@@ -104,7 +104,7 @@ module Gitlab
end end
def exists? def exists?
Gitlab::GitalyClient.migrate(:repository_exists) do |enabled| Gitlab::GitalyClient.migrate(:repository_exists, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled if enabled
gitaly_repository_client.exists? gitaly_repository_client.exists?
else else
......
module Gitlab
module Utils
module StrongMemoize
# Instead of writing patterns like this:
#
# def trigger_from_token
# return @trigger if defined?(@trigger)
#
# @trigger = Ci::Trigger.find_by_token(params[:token].to_s)
# end
#
# We could write it like:
#
# def trigger_from_token
# strong_memoize(:trigger) do
# Ci::Trigger.find_by_token(params[:token].to_s)
# end
# end
#
def strong_memoize(name)
ivar_name = "@#{name}"
if instance_variable_defined?(ivar_name)
instance_variable_get(ivar_name)
else
instance_variable_set(ivar_name, yield)
end
end
end
end
end
...@@ -5,7 +5,7 @@ module QA ...@@ -5,7 +5,7 @@ module QA
include Scenario::Actable include Scenario::Actable
def refresh def refresh
visit current_path visit current_url
end end
end end
end end
......
...@@ -218,18 +218,18 @@ feature 'GFM autocomplete', :js do ...@@ -218,18 +218,18 @@ feature 'GFM autocomplete', :js do
user_item = find('.atwho-view li', text: user.username) user_item = find('.atwho-view li', text: user.username)
expect(user_item).to have_content(user.username) expect(user_item).to have_content(user.username)
end end
end
def expect_to_wrap(should_wrap, item, note, value) def expect_to_wrap(should_wrap, item, note, value)
expect(item).to have_content(value) expect(item).to have_content(value)
expect(item).not_to have_content("\"#{value}\"") expect(item).not_to have_content("\"#{value}\"")
item.click item.click
if should_wrap if should_wrap
expect(note.value).to include("\"#{value}\"") expect(note.value).to include("\"#{value}\"")
else else
expect(note.value).not_to include("\"#{value}\"") expect(note.value).not_to include("\"#{value}\"")
end
end end
end end
end end
...@@ -67,6 +67,28 @@ feature 'Create New Merge Request', :js do ...@@ -67,6 +67,28 @@ feature 'Create New Merge Request', :js do
expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch' expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch'
end end
it 'allows filtering multiple dropdowns' do
visit project_new_merge_request_path(project)
first('.js-source-branch').click
input = find('.dropdown-source-branch .dropdown-input-field')
input.click
input.send_keys('orphaned-branch')
find('.dropdown-source-branch .dropdown-content li', match: :first)
source_items = all('.dropdown-source-branch .dropdown-content li')
expect(source_items.count).to eq(1)
first('.js-target-branch').click
find('.dropdown-target-branch .dropdown-content li', match: :first)
target_items = all('.dropdown-target-branch .dropdown-content li')
expect(target_items.count).to be > 1
end
context 'when target project cannot be viewed by the current user' do context 'when target project cannot be viewed by the current user' do
it 'does not leak the private project name & namespace' do it 'does not leak the private project name & namespace' do
private_project = create(:project, :private, :repository) private_project = create(:project, :private, :repository)
......
...@@ -65,4 +65,33 @@ feature 'Milestone' do ...@@ -65,4 +65,33 @@ feature 'Milestone' do
expect(find('.alert-danger')).to have_content('already being used for another group or project milestone.') expect(find('.alert-danger')).to have_content('already being used for another group or project milestone.')
end end
end end
feature 'Open a milestone' do
scenario 'shows total issue time spent correctly when no time has been logged' do
milestone = create(:milestone, project: project, title: 8.7)
visit project_milestone_path(project, milestone)
page.within('.block.time_spent') do
expect(page).to have_content 'No time spent'
expect(page).to have_content 'None'
end
end
scenario 'shows total issue time spent' do
milestone = create(:milestone, project: project, title: 8.7)
issue1 = create(:issue, project: project, milestone: milestone)
issue2 = create(:issue, project: project, milestone: milestone)
issue1.spend_time(duration: 3600, user: user)
issue1.save!
issue2.spend_time(duration: 7200, user: user)
issue2.save!
visit project_milestone_path(project, milestone)
page.within('.block.time_spent') do
expect(page).to have_content '3h'
end
end
end
end end
...@@ -42,6 +42,21 @@ describe AutocompleteUsersFinder do ...@@ -42,6 +42,21 @@ describe AutocompleteUsersFinder do
it { is_expected.to match_array([user1]) } it { is_expected.to match_array([user1]) }
end end
context 'when passed a subgroup', :nested_groups do
let(:grandparent) { create(:group, :public) }
let(:parent) { create(:group, :public, parent: grandparent) }
let(:child) { create(:group, :public, parent: parent) }
let(:group) { parent }
let!(:grandparent_user) { create(:group_member, :developer, group: grandparent).user }
let!(:parent_user) { create(:group_member, :developer, group: parent).user }
let!(:child_user) { create(:group_member, :developer, group: child).user }
it 'includes users from parent groups as well' do
expect(subject).to match_array([grandparent_user, parent_user])
end
end
it { is_expected.to match_array([user1, external_user, omniauth_user, current_user]) } it { is_expected.to match_array([user1, external_user, omniauth_user, current_user]) }
context 'when filtered by search' do context 'when filtered by search' do
......
{
"type": "object",
"properties": {
"domain": { "type": "string" },
"url": { "type": "uri" },
"certificate_expiration": {
"type": "object",
"properties": {
"expired": { "type": "boolean" },
"expiration": { "type": "string" }
},
"required": ["expired", "expiration"],
"additionalProperties": false
}
},
"required": ["domain", "url"],
"additionalProperties": false
}
{
"type": "object",
"properties": {
"domain": { "type": "string" },
"url": { "type": "uri" },
"certificate": {
"type": "object",
"properties": {
"subject": { "type": "string" },
"expired": { "type": "boolean" },
"certificate": { "type": "string" },
"certificate_text": { "type": "string" }
},
"required": ["subject", "expired"],
"additionalProperties": false
}
},
"required": ["domain", "url"],
"additionalProperties": false
}
{
"type": "array",
"items": { "$ref": "pages_domain/basic.json" }
}
{ {
"type": "array", "type": "array",
"items": { "items": { "$ref": "pages_domain/detail.json" }
"type": "object",
"properties": {
"domain": { "type": "string" },
"url": { "type": "uri" },
"certificate": {
"type": "object",
"properties": {
"subject": { "type": "string" },
"expired": { "type": "boolean" },
"certificate": { "type": "string" },
"certificate_text": { "type": "string" }
},
"required": ["subject", "expired"],
"additionalProperties": false
}
},
"required": ["domain", "url"],
"additionalProperties": false
}
} }
...@@ -67,6 +67,28 @@ describe('GfmAutoComplete', function () { ...@@ -67,6 +67,28 @@ describe('GfmAutoComplete', function () {
}); });
}); });
describe('DefaultOptions.beforeInsert', () => {
const beforeInsert = (context, value) => (
gfmAutoCompleteCallbacks.beforeInsert.call(context, value)
);
const atwhoInstance = { setting: { skipSpecialCharacterTest: false } };
it('should not quote if value only contains alphanumeric charecters', () => {
expect(beforeInsert(atwhoInstance, '@user1')).toBe('@user1');
expect(beforeInsert(atwhoInstance, '~label1')).toBe('~label1');
});
it('should quote if value contains any non-alphanumeric characters', () => {
expect(beforeInsert(atwhoInstance, '~label-20')).toBe('~"label-20"');
expect(beforeInsert(atwhoInstance, '~label 20')).toBe('~"label 20"');
});
it('should quote integer labels', () => {
expect(beforeInsert(atwhoInstance, '~1234')).toBe('~"1234"');
});
});
describe('DefaultOptions.matcher', function () { describe('DefaultOptions.matcher', function () {
const defaultMatcher = (context, flag, subtext) => ( const defaultMatcher = (context, flag, subtext) => (
gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext) gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext)
......
import textUtils from '~/lib/utils/text_markdown';
describe('init markdown', () => {
let textArea;
beforeAll(() => {
textArea = document.createElement('textarea');
document.querySelector('body').appendChild(textArea);
textArea.focus();
});
afterAll(() => {
textArea.parentNode.removeChild(textArea);
});
describe('without selection', () => {
it('inserts the tag on an empty line', () => {
const initialValue = '';
textArea.value = initialValue;
textArea.selectionStart = 0;
textArea.selectionEnd = 0;
textUtils.insertText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}* `);
});
it('inserts the tag on a new line if the current one is not empty', () => {
const initialValue = 'some text';
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
textUtils.insertText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}\n* `);
});
it('inserts the tag on the same line if the current line only contains spaces', () => {
const initialValue = ' ';
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
textUtils.insertText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}* `);
});
it('inserts the tag on the same line if the current line only contains tabs', () => {
const initialValue = '\t\t\t';
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
textUtils.insertText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}* `);
});
});
});
import * as textUtility from '~/lib/utils/text_utility'; import * as textUtils from '~/lib/utils/text_utility';
describe('text_utility', () => { describe('text_utility', () => {
describe('gl.text.getTextWidth', () => { describe('addDelimiter', () => {
it('returns zero width when no text is passed', () => { it('should add a delimiter to the given string', () => {
expect(gl.text.getTextWidth('')).toBe(0); expect(textUtils.addDelimiter('1234')).toEqual('1,234');
expect(textUtils.addDelimiter('222222')).toEqual('222,222');
}); });
it('returns zero width when no text is passed and font is passed', () => { it('should not add a delimiter if string contains no numbers', () => {
expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0); expect(textUtils.addDelimiter('aaaa')).toEqual('aaaa');
});
it('returns width when text is passed', () => {
expect(gl.text.getTextWidth('foo') > 0).toBe(true);
});
it('returns bigger width when font is larger', () => {
const largeFont = gl.text.getTextWidth('foo', '100px sans-serif');
const regular = gl.text.getTextWidth('foo', '10px sans-serif');
expect(largeFont > regular).toBe(true);
});
});
describe('gl.text.pluralize', () => {
it('returns pluralized', () => {
expect(gl.text.pluralize('test', 2)).toBe('tests');
});
it('returns pluralized when count is 0', () => {
expect(gl.text.pluralize('test', 0)).toBe('tests');
});
it('does not return pluralized', () => {
expect(gl.text.pluralize('test', 1)).toBe('test');
}); });
}); });
describe('highCountTrim', () => { describe('highCountTrim', () => {
it('returns 99+ for count >= 100', () => { it('returns 99+ for count >= 100', () => {
expect(textUtility.highCountTrim(105)).toBe('99+'); expect(textUtils.highCountTrim(105)).toBe('99+');
expect(textUtility.highCountTrim(100)).toBe('99+'); expect(textUtils.highCountTrim(100)).toBe('99+');
}); });
it('returns exact number for count < 100', () => { it('returns exact number for count < 100', () => {
expect(textUtility.highCountTrim(45)).toBe(45); expect(textUtils.highCountTrim(45)).toBe(45);
}); });
}); });
describe('capitalizeFirstCharacter', () => { describe('capitalizeFirstCharacter', () => {
it('returns string with first letter capitalized', () => { it('returns string with first letter capitalized', () => {
expect(textUtility.capitalizeFirstCharacter('gitlab')).toEqual('Gitlab'); expect(textUtils.capitalizeFirstCharacter('gitlab')).toEqual('Gitlab');
expect(textUtils.highCountTrim(105)).toBe('99+');
expect(textUtils.highCountTrim(100)).toBe('99+');
}); });
}); });
describe('gl.text.insertText', () => { describe('humanize', () => {
let textArea; it('should remove underscores and uppercase the first letter', () => {
expect(textUtils.humanize('foo_bar')).toEqual('Foo bar');
beforeAll(() => {
textArea = document.createElement('textarea');
document.querySelector('body').appendChild(textArea);
textArea.focus();
}); });
});
afterAll(() => { describe('pluralize', () => {
textArea.parentNode.removeChild(textArea); it('should pluralize given string', () => {
expect(textUtils.pluralize('test', 2)).toBe('tests');
}); });
describe('without selection', () => { it('should pluralize when count is 0', () => {
it('inserts the tag on an empty line', () => { expect(textUtils.pluralize('test', 0)).toBe('tests');
const initialValue = ''; });
textArea.value = initialValue;
textArea.selectionStart = 0;
textArea.selectionEnd = 0;
gl.text.insertText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}* `);
});
it('inserts the tag on a new line if the current one is not empty', () => {
const initialValue = 'some text';
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
gl.text.insertText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}\n* `);
});
it('inserts the tag on the same line if the current line only contains spaces', () => {
const initialValue = ' ';
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
gl.text.insertText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}* `);
});
it('inserts the tag on the same line if the current line only contains tabs', () => {
const initialValue = '\t\t\t';
textArea.value = initialValue; it('should not pluralize when count is 1', () => {
textArea.setSelectionRange(initialValue.length, initialValue.length); expect(textUtils.pluralize('test', 1)).toBe('test');
});
});
gl.text.insertText(textArea, textArea.value, '*', null, '', false); describe('dasherize', () => {
it('should replace underscores with dashes', () => {
expect(textUtils.dasherize('foo_bar_foo')).toEqual('foo-bar-foo');
});
});
expect(textArea.value).toEqual(`${initialValue}* `); describe('slugify', () => {
}); it('should remove accents and convert to lower case', () => {
expect(textUtils.slugify('João')).toEqual('joão');
}); });
}); });
}); });
...@@ -12,9 +12,4 @@ export const file = (name = 'name', id = name, type = '') => decorateData({ ...@@ -12,9 +12,4 @@ export const file = (name = 'name', id = name, type = '') => decorateData({
url: 'url', url: 'url',
name, name,
path: name, path: name,
last_commit: {
id: '123',
message: 'test',
committed_date: new Date().toISOString(),
},
}); });
import store from '~/repo/stores';
import service from '~/repo/services';
import { resetStore } from '../../helpers';
describe('Multi-file store branch actions', () => {
afterEach(() => {
resetStore(store);
});
describe('createNewBranch', () => {
beforeEach(() => {
spyOn(service, 'createBranch').and.returnValue(Promise.resolve({
json: () => ({
name: 'testing',
}),
}));
spyOn(history, 'pushState');
store.state.project.id = 2;
store.state.currentBranch = 'testing';
});
it('creates new branch', (done) => {
store.dispatch('createNewBranch', 'master')
.then(() => {
expect(store.state.currentBranch).toBe('testing');
expect(service.createBranch).toHaveBeenCalledWith(2, {
branch: 'master',
ref: 'testing',
});
expect(history.pushState).toHaveBeenCalled();
done();
})
.catch(done.fail);
});
});
});
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import * as getters from '~/repo/stores/getters';
import state from '~/repo/stores/state';
import { file } from '../helpers';
describe('Multi-file store getters', () => {
let localState;
beforeEach(() => {
localState = state();
});
describe('treeList', () => {
it('returns flat tree list', () => {
localState.tree.push(file('1'));
localState.tree[0].tree.push(file('2'));
localState.tree[0].tree[0].tree.push(file('3'));
const treeList = getters.treeList(localState);
expect(treeList.length).toBe(3);
expect(treeList[1].name).toBe(localState.tree[0].tree[0].name);
expect(treeList[2].name).toBe(localState.tree[0].tree[0].tree[0].name);
});
});
describe('changedFiles', () => {
it('returns a list of changed opened files', () => {
localState.openFiles.push(file());
localState.openFiles.push(file('changed'));
localState.openFiles[1].changed = true;
const changedFiles = getters.changedFiles(localState);
expect(changedFiles.length).toBe(1);
expect(changedFiles[0].name).toBe('changed');
});
});
describe('activeFile', () => {
it('returns the current active file', () => {
localState.openFiles.push(file());
localState.openFiles.push(file('active'));
localState.openFiles[1].active = true;
expect(getters.activeFile(localState).name).toBe('active');
});
it('returns undefined if no active files are found', () => {
localState.openFiles.push(file());
localState.openFiles.push(file('active'));
expect(getters.activeFile(localState)).toBeUndefined();
});
});
describe('activeFileExtension', () => {
it('returns the file extension for the current active file', () => {
localState.openFiles.push(file('active'));
localState.openFiles[0].active = true;
localState.openFiles[0].path = 'test.js';
expect(getters.activeFileExtension(localState)).toBe('.js');
localState.openFiles[0].path = 'test.es6.js';
expect(getters.activeFileExtension(localState)).toBe('.js');
});
});
describe('isCollapsed', () => {
it('returns true if state has open files', () => {
localState.openFiles.push(file());
expect(getters.isCollapsed(localState)).toBeTruthy();
});
it('returns false if state has no open files', () => {
expect(getters.isCollapsed(localState)).toBeFalsy();
});
});
describe('canEditFile', () => {
beforeEach(() => {
localState.onTopOfBranch = true;
localState.canCommit = true;
localState.openFiles.push(file());
localState.openFiles[0].active = true;
});
it('returns true if user can commit and has open files', () => {
expect(getters.canEditFile(localState)).toBeTruthy();
});
it('returns false if user can commit and has no open files', () => {
localState.openFiles = [];
expect(getters.canEditFile(localState)).toBeFalsy();
});
it('returns false if user can commit and active file is binary', () => {
localState.openFiles[0].binary = true;
expect(getters.canEditFile(localState)).toBeFalsy();
});
it('returns false if user cant commit', () => {
localState.canCommit = false;
expect(getters.canEditFile(localState)).toBeFalsy();
});
it('returns false if user can commit but on a branch', () => {
localState.onTopOfBranch = false;
expect(getters.canEditFile(localState)).toBeFalsy();
});
});
});
import mutations from '~/repo/stores/mutations/branch';
import state from '~/repo/stores/state';
describe('Multi-file store branch mutations', () => {
let localState;
beforeEach(() => {
localState = state();
});
describe('SET_CURRENT_BRANCH', () => {
it('sets currentBranch', () => {
mutations.SET_CURRENT_BRANCH(localState, 'master');
expect(localState.currentBranch).toBe('master');
});
});
});
import mutations from '~/repo/stores/mutations/file';
import state from '~/repo/stores/state';
import { file } from '../../helpers';
describe('Multi-file store file mutations', () => {
let localState;
let localFile;
beforeEach(() => {
localState = state();
localFile = file();
});
describe('SET_FILE_ACTIVE', () => {
it('sets the file active', () => {
mutations.SET_FILE_ACTIVE(localState, {
file: localFile,
active: true,
});
expect(localFile.active).toBeTruthy();
});
});
describe('TOGGLE_FILE_OPEN', () => {
beforeEach(() => {
mutations.TOGGLE_FILE_OPEN(localState, localFile);
});
it('adds into opened files', () => {
expect(localFile.opened).toBeTruthy();
expect(localState.openFiles.length).toBe(1);
});
it('removes from opened files', () => {
mutations.TOGGLE_FILE_OPEN(localState, localFile);
expect(localFile.opened).toBeFalsy();
expect(localState.openFiles.length).toBe(0);
});
});
describe('SET_FILE_DATA', () => {
it('sets extra file data', () => {
mutations.SET_FILE_DATA(localState, {
data: {
blame_path: 'blame',
commits_path: 'commits',
permalink: 'permalink',
raw_path: 'raw',
binary: true,
html: 'html',
render_error: 'render_error',
},
file: localFile,
});
expect(localFile.blamePath).toBe('blame');
expect(localFile.commitsPath).toBe('commits');
expect(localFile.permalink).toBe('permalink');
expect(localFile.rawPath).toBe('raw');
expect(localFile.binary).toBeTruthy();
expect(localFile.html).toBe('html');
expect(localFile.renderError).toBe('render_error');
});
});
describe('SET_FILE_RAW_DATA', () => {
it('sets raw data', () => {
mutations.SET_FILE_RAW_DATA(localState, {
file: localFile,
raw: 'testing',
});
expect(localFile.raw).toBe('testing');
});
});
describe('UPDATE_FILE_CONTENT', () => {
beforeEach(() => {
localFile.raw = 'test';
});
it('sets content', () => {
mutations.UPDATE_FILE_CONTENT(localState, {
file: localFile,
content: 'test',
});
expect(localFile.content).toBe('test');
});
it('sets changed if content does not match raw', () => {
mutations.UPDATE_FILE_CONTENT(localState, {
file: localFile,
content: 'testing',
});
expect(localFile.content).toBe('testing');
expect(localFile.changed).toBeTruthy();
});
});
describe('DISCARD_FILE_CHANGES', () => {
beforeEach(() => {
localFile.content = 'test';
localFile.changed = true;
});
it('resets content and changed', () => {
mutations.DISCARD_FILE_CHANGES(localState, localFile);
expect(localFile.content).toBe('');
expect(localFile.changed).toBeFalsy();
});
});
describe('CREATE_TMP_FILE', () => {
it('adds file into parent tree', () => {
const f = file();
mutations.CREATE_TMP_FILE(localState, {
file: f,
parent: localFile,
});
expect(localFile.tree.length).toBe(1);
expect(localFile.tree[0].name).toBe(f.name);
});
});
});
import mutations from '~/repo/stores/mutations/tree';
import state from '~/repo/stores/state';
import { file } from '../../helpers';
describe('Multi-file store tree mutations', () => {
let localState;
let localTree;
beforeEach(() => {
localState = state();
localTree = file();
});
describe('TOGGLE_TREE_OPEN', () => {
it('toggles tree open', () => {
mutations.TOGGLE_TREE_OPEN(localState, localTree);
expect(localTree.opened).toBeTruthy();
mutations.TOGGLE_TREE_OPEN(localState, localTree);
expect(localTree.opened).toBeFalsy();
});
});
describe('SET_DIRECTORY_DATA', () => {
const data = [{
name: 'tree',
},
{
name: 'submodule',
},
{
name: 'blob',
}];
it('adds directory data', () => {
mutations.SET_DIRECTORY_DATA(localState, {
data,
tree: localState,
});
expect(localState.tree.length).toBe(3);
expect(localState.tree[0].name).toBe('tree');
expect(localState.tree[1].name).toBe('submodule');
expect(localState.tree[2].name).toBe('blob');
});
});
describe('SET_PARENT_TREE_URL', () => {
it('sets the parent tree url', () => {
mutations.SET_PARENT_TREE_URL(localState, 'test');
expect(localState.parentTreeUrl).toBe('test');
});
});
describe('CREATE_TMP_TREE', () => {
it('adds tree into parent tree', () => {
const tmpEntry = file();
mutations.CREATE_TMP_TREE(localState, {
tmpEntry,
parent: localTree,
});
expect(localTree.tree.length).toBe(1);
expect(localTree.tree[0].name).toBe(tmpEntry.name);
});
});
});
import mutations from '~/repo/stores/mutations';
import state from '~/repo/stores/state';
import { file } from '../helpers';
describe('Multi-file store mutations', () => {
let localState;
let entry;
beforeEach(() => {
localState = state();
entry = file();
});
describe('SET_INITIAL_DATA', () => {
it('sets all initial data', () => {
mutations.SET_INITIAL_DATA(localState, {
test: 'test',
});
expect(localState.test).toBe('test');
});
});
describe('SET_PREVIEW_MODE', () => {
it('sets currentBlobView to repo-preview', () => {
mutations.SET_PREVIEW_MODE(localState);
expect(localState.currentBlobView).toBe('repo-preview');
localState.currentBlobView = 'testing';
mutations.SET_PREVIEW_MODE(localState);
expect(localState.currentBlobView).toBe('repo-preview');
});
});
describe('SET_EDIT_MODE', () => {
it('sets currentBlobView to repo-editor', () => {
mutations.SET_EDIT_MODE(localState);
expect(localState.currentBlobView).toBe('repo-editor');
localState.currentBlobView = 'testing';
mutations.SET_EDIT_MODE(localState);
expect(localState.currentBlobView).toBe('repo-editor');
});
});
describe('TOGGLE_LOADING', () => {
it('toggles loading of entry', () => {
mutations.TOGGLE_LOADING(localState, entry);
expect(entry.loading).toBeTruthy();
mutations.TOGGLE_LOADING(localState, entry);
expect(entry.loading).toBeFalsy();
});
});
describe('TOGGLE_EDIT_MODE', () => {
it('toggles editMode', () => {
mutations.TOGGLE_EDIT_MODE(localState);
expect(localState.editMode).toBeTruthy();
mutations.TOGGLE_EDIT_MODE(localState);
expect(localState.editMode).toBeFalsy();
});
});
describe('TOGGLE_DISCARD_POPUP', () => {
it('sets discardPopupOpen', () => {
mutations.TOGGLE_DISCARD_POPUP(localState, true);
expect(localState.discardPopupOpen).toBeTruthy();
mutations.TOGGLE_DISCARD_POPUP(localState, false);
expect(localState.discardPopupOpen).toBeFalsy();
});
});
describe('SET_COMMIT_REF', () => {
it('sets currentRef', () => {
mutations.SET_COMMIT_REF(localState, '123');
expect(localState.currentRef).toBe('123');
});
});
describe('SET_ROOT', () => {
it('sets isRoot & initialRoot', () => {
mutations.SET_ROOT(localState, true);
expect(localState.isRoot).toBeTruthy();
expect(localState.isInitialRoot).toBeTruthy();
mutations.SET_ROOT(localState, false);
expect(localState.isRoot).toBeFalsy();
expect(localState.isInitialRoot).toBeFalsy();
});
});
describe('SET_PREVIOUS_URL', () => {
it('sets previousUrl', () => {
mutations.SET_PREVIOUS_URL(localState, 'testing');
expect(localState.previousUrl).toBe('testing');
});
});
});
import * as utils from '~/repo/stores/utils';
describe('Multi-file store utils', () => {
describe('setPageTitle', () => {
it('sets the document page title', () => {
utils.setPageTitle('test');
expect(document.title).toBe('test');
});
});
describe('pushState', () => {
it('calls history.pushState', () => {
spyOn(history, 'pushState');
utils.pushState('test');
expect(history.pushState).toHaveBeenCalledWith({ url: 'test' }, '', 'test');
});
});
describe('createTemp', () => {
it('creates temp tree', () => {
const tmp = utils.createTemp({
name: 'test',
path: 'test',
type: 'tree',
level: 0,
changed: false,
content: '',
base64: '',
});
expect(tmp.tempFile).toBeTruthy();
expect(tmp.icon).toBe('fa-folder');
});
it('creates temp file', () => {
const tmp = utils.createTemp({
name: 'test',
path: 'test',
type: 'blob',
level: 0,
changed: false,
content: '',
base64: '',
});
expect(tmp.tempFile).toBeTruthy();
expect(tmp.icon).toBe('fa-file-text-o');
});
});
describe('findIndexOfFile', () => {
let state;
beforeEach(() => {
state = [{
path: '1',
}, {
path: '2',
}];
});
it('finds in the index of an entry by path', () => {
const index = utils.findIndexOfFile(state, {
path: '2',
});
expect(index).toBe(1);
});
});
describe('findEntry', () => {
let state;
beforeEach(() => {
state = {
tree: [{
type: 'tree',
name: 'test',
}, {
type: 'blob',
name: 'file',
}],
};
});
it('returns an entry found by name', () => {
const foundEntry = utils.findEntry(state, 'tree', 'test');
expect(foundEntry.type).toBe('tree');
expect(foundEntry.name).toBe('test');
});
it('returns undefined when no entry found', () => {
const foundEntry = utils.findEntry(state, 'blob', 'test');
expect(foundEntry).toBeUndefined();
});
});
});
require 'spec_helper'
describe Gitlab::Utils::StrongMemoize do
let(:klass) do
struct = Struct.new(:value) do
def method_name
strong_memoize(:method_name) do
trace << value
value
end
end
def trace
@trace ||= []
end
end
struct.include(described_class)
struct
end
subject(:object) { klass.new(value) }
shared_examples 'caching the value' do
it 'only calls the block once' do
value0 = object.method_name
value1 = object.method_name
expect(value0).to eq(value)
expect(value1).to eq(value)
expect(object.trace).to contain_exactly(value)
end
it 'returns and defines the instance variable for the exact value' do
returned_value = object.method_name
memoized_value = object.instance_variable_get(:@method_name)
expect(returned_value).to eql(value)
expect(memoized_value).to eql(value)
end
end
describe '#strong_memoize' do
[nil, false, true, 'value', 0, [0]].each do |value|
context "with value #{value}" do
let(:value) { value }
it_behaves_like 'caching the value'
end
end
end
end
...@@ -783,7 +783,25 @@ describe Notify do ...@@ -783,7 +783,25 @@ describe Notify do
shared_examples 'an email for a note on a diff discussion' do |model| shared_examples 'an email for a note on a diff discussion' do |model|
let(:note) { create(model, author: note_author) } let(:note) { create(model, author: note_author) }
it "includes diffs with character-level highlighting" do context 'when note is on image' do
before do
allow_any_instance_of(DiffDiscussion).to receive(:on_image?).and_return(true)
end
it 'does not include diffs with character-level highlighting' do
is_expected.not_to have_body_text '<span class="p">}</span></span>'
end
it 'ends the intro with a dot' do
is_expected.to have_body_text "#{note.diff_file.file_path}</a>."
end
end
it 'ends the intro with a colon' do
is_expected.to have_body_text "#{note.diff_file.file_path}</a>:"
end
it 'includes diffs with character-level highlighting' do
is_expected.to have_body_text '<span class="p">}</span></span>' is_expected.to have_body_text '<span class="p">}</span></span>'
end end
......
...@@ -186,4 +186,21 @@ describe Milestone, 'Milestoneish' do ...@@ -186,4 +186,21 @@ describe Milestone, 'Milestoneish' do
expect(milestone.elapsed_days).to eq(2) expect(milestone.elapsed_days).to eq(2)
end end
end end
describe '#total_issue_time_spent' do
it 'calculates total issue time spent' do
closed_issue_1.spend_time(duration: 300, user: author)
closed_issue_1.save!
closed_issue_2.spend_time(duration: 600, user: assignee)
closed_issue_2.save!
expect(milestone.total_issue_time_spent).to eq(900)
end
end
describe '#human_total_issue_time_spent' do
it 'returns nil if no time has been spent' do
expect(milestone.human_total_issue_time_spent).to be_nil
end
end
end end
...@@ -765,22 +765,4 @@ describe Issue do ...@@ -765,22 +765,4 @@ describe Issue do
expect(described_class.public_only).to eq([public_issue]) expect(described_class.public_only).to eq([public_issue])
end end
end end
describe '#update_project_counter_caches?' do
it 'returns true when the state changes' do
subject.state = 'closed'
expect(subject.update_project_counter_caches?).to eq(true)
end
it 'returns true when the confidential flag changes' do
subject.confidential = true
expect(subject.update_project_counter_caches?).to eq(true)
end
it 'returns false when the state or confidential flag did not change' do
expect(subject.update_project_counter_caches?).to eq(false)
end
end
end end
...@@ -1772,16 +1772,4 @@ describe MergeRequest do ...@@ -1772,16 +1772,4 @@ describe MergeRequest do
.to change { project.open_merge_requests_count }.from(1).to(0) .to change { project.open_merge_requests_count }.from(1).to(0)
end end
end end
describe '#update_project_counter_caches?' do
it 'returns true when the state changes' do
subject.state = 'closed'
expect(subject.update_project_counter_caches?).to eq(true)
end
it 'returns false when the state did not change' do
expect(subject.update_project_counter_caches?).to eq(false)
end
end
end end
...@@ -3,6 +3,7 @@ require 'rails_helper' ...@@ -3,6 +3,7 @@ require 'rails_helper'
describe API::PagesDomains do describe API::PagesDomains do
set(:project) { create(:project) } set(:project) { create(:project) }
set(:user) { create(:user) } set(:user) { create(:user) }
set(:admin) { create(:admin) }
set(:pages_domain) { create(:pages_domain, domain: 'www.domain.test', project: project) } set(:pages_domain) { create(:pages_domain, domain: 'www.domain.test', project: project) }
set(:pages_domain_secure) { create(:pages_domain, :with_certificate, :with_key, domain: 'ssl.domain.test', project: project) } set(:pages_domain_secure) { create(:pages_domain, :with_certificate, :with_key, domain: 'ssl.domain.test', project: project) }
...@@ -23,12 +24,49 @@ describe API::PagesDomains do ...@@ -23,12 +24,49 @@ describe API::PagesDomains do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true) allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
end end
describe 'GET /pages/domains' do
context 'when pages is disabled' do
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
end
it_behaves_like '404 response' do
let(:request) { get api('/pages/domains', admin) }
end
end
context 'when pages is enabled' do
context 'when authenticated as an admin' do
it 'returns paginated all pages domains' do
get api('/pages/domains', admin)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domain_basics')
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
expect(json_response.last).to have_key('domain')
expect(json_response.last).to have_key('certificate_expiration')
expect(json_response.last['certificate_expiration']['expired']).to be true
expect(json_response.first).not_to have_key('certificate_expiration')
end
end
context 'when authenticated as a non-member' do
it_behaves_like '403 response' do
let(:request) { get api('/pages/domains', user) }
end
end
end
end
describe 'GET /projects/:project_id/pages/domains' do describe 'GET /projects/:project_id/pages/domains' do
shared_examples_for 'get pages domains' do shared_examples_for 'get pages domains' do
it 'returns paginated pages domains' do it 'returns paginated pages domains' do
get api(route, user) get api(route, user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domains')
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.size).to eq(3) expect(json_response.size).to eq(3)
...@@ -99,6 +137,7 @@ describe API::PagesDomains do ...@@ -99,6 +137,7 @@ describe API::PagesDomains do
get api(route_domain, user) get api(route_domain, user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(json_response['domain']).to eq(pages_domain.domain) expect(json_response['domain']).to eq(pages_domain.domain)
expect(json_response['url']).to eq(pages_domain.url) expect(json_response['url']).to eq(pages_domain.url)
expect(json_response['certificate']).to be_nil expect(json_response['certificate']).to be_nil
...@@ -108,6 +147,7 @@ describe API::PagesDomains do ...@@ -108,6 +147,7 @@ describe API::PagesDomains do
get api(route_secure_domain, user) get api(route_secure_domain, user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(json_response['domain']).to eq(pages_domain_secure.domain) expect(json_response['domain']).to eq(pages_domain_secure.domain)
expect(json_response['url']).to eq(pages_domain_secure.url) expect(json_response['url']).to eq(pages_domain_secure.url)
expect(json_response['certificate']['subject']).to eq(pages_domain_secure.subject) expect(json_response['certificate']['subject']).to eq(pages_domain_secure.subject)
...@@ -118,6 +158,7 @@ describe API::PagesDomains do ...@@ -118,6 +158,7 @@ describe API::PagesDomains do
get api(route_expired_domain, user) get api(route_expired_domain, user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(json_response['certificate']['expired']).to be true expect(json_response['certificate']['expired']).to be true
end end
end end
...@@ -187,6 +228,7 @@ describe API::PagesDomains do ...@@ -187,6 +228,7 @@ describe API::PagesDomains do
pages_domain = PagesDomain.find_by(domain: json_response['domain']) pages_domain = PagesDomain.find_by(domain: json_response['domain'])
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(201)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain.domain).to eq(params[:domain]) expect(pages_domain.domain).to eq(params[:domain])
expect(pages_domain.certificate).to be_nil expect(pages_domain.certificate).to be_nil
expect(pages_domain.key).to be_nil expect(pages_domain.key).to be_nil
...@@ -197,6 +239,7 @@ describe API::PagesDomains do ...@@ -197,6 +239,7 @@ describe API::PagesDomains do
pages_domain = PagesDomain.find_by(domain: json_response['domain']) pages_domain = PagesDomain.find_by(domain: json_response['domain'])
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(201)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain.domain).to eq(params_secure[:domain]) expect(pages_domain.domain).to eq(params_secure[:domain])
expect(pages_domain.certificate).to eq(params_secure[:certificate]) expect(pages_domain.certificate).to eq(params_secure[:certificate])
expect(pages_domain.key).to eq(params_secure[:key]) expect(pages_domain.key).to eq(params_secure[:key])
...@@ -270,6 +313,7 @@ describe API::PagesDomains do ...@@ -270,6 +313,7 @@ describe API::PagesDomains do
pages_domain_secure.reload pages_domain_secure.reload
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain_secure.certificate).to be_nil expect(pages_domain_secure.certificate).to be_nil
expect(pages_domain_secure.key).to be_nil expect(pages_domain_secure.key).to be_nil
end end
...@@ -279,6 +323,7 @@ describe API::PagesDomains do ...@@ -279,6 +323,7 @@ describe API::PagesDomains do
pages_domain.reload pages_domain.reload
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain.certificate).to eq(params_secure[:certificate]) expect(pages_domain.certificate).to eq(params_secure[:certificate])
expect(pages_domain.key).to eq(params_secure[:key]) expect(pages_domain.key).to eq(params_secure[:key])
end end
...@@ -288,6 +333,7 @@ describe API::PagesDomains do ...@@ -288,6 +333,7 @@ describe API::PagesDomains do
pages_domain_expired.reload pages_domain_expired.reload
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain_expired.certificate).to eq(params_secure[:certificate]) expect(pages_domain_expired.certificate).to eq(params_secure[:certificate])
expect(pages_domain_expired.key).to eq(params_secure[:key]) expect(pages_domain_expired.key).to eq(params_secure[:key])
end end
...@@ -297,6 +343,7 @@ describe API::PagesDomains do ...@@ -297,6 +343,7 @@ describe API::PagesDomains do
pages_domain_secure.reload pages_domain_secure.reload
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain_secure.certificate).to eq(params_secure_nokey[:certificate]) expect(pages_domain_secure.certificate).to eq(params_secure_nokey[:certificate])
end end
......
// Converting text to basic latin (aka removing accents)
//
// Based on: http://semplicewebsites.com/removing-accents-javascript
//
var Latinise = {
map: {"Á":"A","Ă":"A","":"A","":"A","":"A","":"A","":"A","Ǎ":"A","Â":"A","":"A","":"A","":"A","":"A","":"A","Ä":"A","Ǟ":"A","Ȧ":"A","Ǡ":"A","":"A","Ȁ":"A","À":"A","":"A","Ȃ":"A","Ā":"A","Ą":"A","Å":"A","Ǻ":"A","":"A","Ⱥ":"A","Ã":"A","":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","":"AO","":"AU","":"AV","":"AV","":"AY","":"B","":"B","Ɓ":"B","":"B","Ƀ":"B","Ƃ":"B","Ć":"C","Č":"C","Ç":"C","":"C","Ĉ":"C","Ċ":"C","Ƈ":"C","Ȼ":"C","Ď":"D","":"D","":"D","":"D","":"D","Ɗ":"D","":"D","Dz":"D","Dž":"D","Đ":"D","Ƌ":"D","DZ":"DZ","DŽ":"DZ","É":"E","Ĕ":"E","Ě":"E","Ȩ":"E","":"E","Ê":"E","":"E","":"E","":"E","":"E","":"E","":"E","Ë":"E","Ė":"E","":"E","Ȅ":"E","È":"E","":"E","Ȇ":"E","Ē":"E","":"E","":"E","Ę":"E","Ɇ":"E","":"E","":"E","":"ET","":"F","Ƒ":"F","Ǵ":"G","Ğ":"G","Ǧ":"G","Ģ":"G","Ĝ":"G","Ġ":"G","Ɠ":"G","":"G","Ǥ":"G","":"H","Ȟ":"H","":"H","Ĥ":"H","":"H","":"H","":"H","":"H","Ħ":"H","Í":"I","Ĭ":"I","Ǐ":"I","Î":"I","Ï":"I","":"I","İ":"I","":"I","Ȉ":"I","Ì":"I","":"I","Ȋ":"I","Ī":"I","Į":"I","Ɨ":"I","Ĩ":"I","":"I","":"D","":"F","":"G","":"R","":"S","":"T","":"IS","Ĵ":"J","Ɉ":"J","":"K","Ǩ":"K","Ķ":"K","":"K","":"K","":"K","Ƙ":"K","":"K","":"K","":"K","Ĺ":"L","Ƚ":"L","Ľ":"L","Ļ":"L","":"L","":"L","":"L","":"L","":"L","":"L","Ŀ":"L","":"L","Lj":"L","Ł":"L","LJ":"LJ","":"M","":"M","":"M","":"M","Ń":"N","Ň":"N","Ņ":"N","":"N","":"N","":"N","Ǹ":"N","Ɲ":"N","":"N","Ƞ":"N","Nj":"N","Ñ":"N","NJ":"NJ","Ó":"O","Ŏ":"O","Ǒ":"O","Ô":"O","":"O","":"O","":"O","":"O","":"O","Ö":"O","Ȫ":"O","Ȯ":"O","Ȱ":"O","":"O","Ő":"O","Ȍ":"O","Ò":"O","":"O","Ơ":"O","":"O","":"O","":"O","":"O","":"O","Ȏ":"O","":"O","":"O","Ō":"O","":"O","":"O","Ɵ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Õ":"O","":"O","":"O","Ȭ":"O","Ƣ":"OI","":"OO","Ɛ":"E","Ɔ":"O","Ȣ":"OU","":"P","":"P","":"P","Ƥ":"P","":"P","":"P","":"P","":"Q","":"Q","Ŕ":"R","Ř":"R","Ŗ":"R","":"R","":"R","":"R","Ȑ":"R","Ȓ":"R","":"R","Ɍ":"R","":"R","":"C","Ǝ":"E","Ś":"S","":"S","Š":"S","":"S","Ş":"S","Ŝ":"S","Ș":"S","":"S","":"S","":"S","":"SS","Ť":"T","Ţ":"T","":"T","Ț":"T","Ⱦ":"T","":"T","":"T","Ƭ":"T","":"T","Ʈ":"T","Ŧ":"T","":"A","":"L","Ɯ":"M","Ʌ":"V","":"TZ","Ú":"U","Ŭ":"U","Ǔ":"U","Û":"U","":"U","Ü":"U","Ǘ":"U","Ǚ":"U","Ǜ":"U","Ǖ":"U","":"U","":"U","Ű":"U","Ȕ":"U","Ù":"U","":"U","Ư":"U","":"U","":"U","":"U","":"U","":"U","Ȗ":"U","Ū":"U","":"U","Ų":"U","Ů":"U","Ũ":"U","":"U","":"U","":"V","":"V","Ʋ":"V","":"V","":"VY","":"W","Ŵ":"W","":"W","":"W","":"W","":"W","":"W","":"X","":"X","Ý":"Y","Ŷ":"Y","Ÿ":"Y","":"Y","":"Y","":"Y","Ƴ":"Y","":"Y","":"Y","Ȳ":"Y","Ɏ":"Y","":"Y","Ź":"Z","Ž":"Z","":"Z","":"Z","Ż":"Z","":"Z","Ȥ":"Z","":"Z","Ƶ":"Z","IJ":"IJ","Œ":"OE","":"A","":"AE","ʙ":"B","":"B","":"C","":"D","":"E","":"F","ɢ":"G","ʛ":"G","ʜ":"H","ɪ":"I","ʁ":"R","":"J","":"K","ʟ":"L","":"L","":"M","ɴ":"N","":"O","ɶ":"OE","":"O","":"OU","":"P","ʀ":"R","":"N","":"R","":"S","":"T","":"E","":"R","":"U","":"V","":"W","ʏ":"Y","":"Z","á":"a","ă":"a","":"a","":"a","":"a","":"a","":"a","ǎ":"a","â":"a","":"a","":"a","":"a","":"a","":"a","ä":"a","ǟ":"a","ȧ":"a","ǡ":"a","":"a","ȁ":"a","à":"a","":"a","ȃ":"a","ā":"a","ą":"a","":"a","":"a","å":"a","ǻ":"a","":"a","":"a","ã":"a","":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","":"ao","":"au","":"av","":"av","":"ay","":"b","":"b","ɓ":"b","":"b","":"b","":"b","ƀ":"b","ƃ":"b","ɵ":"o","ć":"c","č":"c","ç":"c","":"c","ĉ":"c","ɕ":"c","ċ":"c","ƈ":"c","ȼ":"c","ď":"d","":"d","":"d","ȡ":"d","":"d","":"d","ɗ":"d","":"d","":"d","":"d","":"d","đ":"d","ɖ":"d","ƌ":"d","ı":"i","ȷ":"j","ɟ":"j","ʄ":"j","dz":"dz","dž":"dz","é":"e","ĕ":"e","ě":"e","ȩ":"e","":"e","ê":"e","ế":"e","":"e","":"e","":"e","":"e","":"e","ë":"e","ė":"e","":"e","ȅ":"e","è":"e","":"e","ȇ":"e","ē":"e","":"e","":"e","":"e","ę":"e","":"e","ɇ":"e","":"e","":"e","":"et","":"f","ƒ":"f","":"f","":"f","ǵ":"g","ğ":"g","ǧ":"g","ģ":"g","ĝ":"g","ġ":"g","ɠ":"g","":"g","":"g","ǥ":"g","":"h","ȟ":"h","":"h","ĥ":"h","":"h","":"h","":"h","":"h","ɦ":"h","":"h","ħ":"h","ƕ":"hv","í":"i","ĭ":"i","ǐ":"i","î":"i","ï":"i","":"i","":"i","ȉ":"i","ì":"i","":"i","ȋ":"i","ī":"i","į":"i","":"i","ɨ":"i","ĩ":"i","":"i","":"d","":"f","":"g","":"r","":"s","":"t","":"is","ǰ":"j","ĵ":"j","ʝ":"j","ɉ":"j","":"k","ǩ":"k","ķ":"k","":"k","":"k","":"k","ƙ":"k","":"k","":"k","":"k","":"k","ĺ":"l","ƚ":"l","ɬ":"l","ľ":"l","ļ":"l","":"l","ȴ":"l","":"l","":"l","":"l","":"l","":"l","ŀ":"l","ɫ":"l","":"l","ɭ":"l","ł":"l","lj":"lj","ſ":"s","":"s","":"s","":"s","ḿ":"m","":"m","":"m","ɱ":"m","":"m","":"m","ń":"n","ň":"n","ņ":"n","":"n","ȵ":"n","":"n","":"n","ǹ":"n","ɲ":"n","":"n","ƞ":"n","":"n","":"n","ɳ":"n","ñ":"n","nj":"nj","ó":"o","ŏ":"o","ǒ":"o","ô":"o","":"o","":"o","":"o","":"o","":"o","ö":"o","ȫ":"o","ȯ":"o","ȱ":"o","":"o","ő":"o","ȍ":"o","ò":"o","":"o","ơ":"o","":"o","":"o","":"o","":"o","":"o","ȏ":"o","":"o","":"o","":"o","ō":"o","":"o","":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","õ":"o","":"o","":"o","ȭ":"o","ƣ":"oi","":"oo","ɛ":"e","":"e","ɔ":"o","":"o","ȣ":"ou","":"p","":"p","":"p","ƥ":"p","":"p","":"p","":"p","":"p","":"p","":"q","ʠ":"q","ɋ":"q","":"q","ŕ":"r","ř":"r","ŗ":"r","":"r","":"r","":"r","ȑ":"r","ɾ":"r","":"r","ȓ":"r","":"r","ɼ":"r","":"r","":"r","ɍ":"r","ɽ":"r","":"c","":"c","ɘ":"e","ɿ":"r","ś":"s","":"s","š":"s","":"s","ş":"s","ŝ":"s","ș":"s","":"s","":"s","":"s","ʂ":"s","":"s","":"s","ȿ":"s","ɡ":"g","ß":"ss","":"o","":"o","":"u","ť":"t","ţ":"t","":"t","ț":"t","ȶ":"t","":"t","":"t","":"t","":"t","ƭ":"t","":"t","":"t","ƫ":"t","ʈ":"t","ŧ":"t","":"th","ɐ":"a","":"ae","ǝ":"e","":"g","ɥ":"h","ʮ":"h","ʯ":"h","":"i","ʞ":"k","":"l","ɯ":"m","ɰ":"m","":"oe","ɹ":"r","ɻ":"r","ɺ":"r","":"r","ʇ":"t","ʌ":"v","ʍ":"w","ʎ":"y","":"tz","ú":"u","ŭ":"u","ǔ":"u","û":"u","":"u","ü":"u","ǘ":"u","ǚ":"u","ǜ":"u","ǖ":"u","":"u","":"u","ű":"u","ȕ":"u","ù":"u","":"u","ư":"u","":"u","":"u","":"u","":"u","":"u","ȗ":"u","ū":"u","":"u","ų":"u","":"u","ů":"u","ũ":"u","":"u","":"u","":"ue","":"um","":"v","":"v","ṿ":"v","ʋ":"v","":"v","":"v","":"v","":"vy","":"w","ŵ":"w","":"w","":"w","":"w","":"w","":"w","":"w","":"x","":"x","":"x","ý":"y","ŷ":"y","ÿ":"y","":"y","":"y","":"y","ƴ":"y","":"y","ỿ":"y","ȳ":"y","":"y","ɏ":"y","":"y","ź":"z","ž":"z","":"z","ʑ":"z","":"z","ż":"z","":"z","ȥ":"z","":"z","":"z","":"z","ʐ":"z","ƶ":"z","ɀ":"z","":"ff","":"ffi","":"ffl","":"fi","":"fl","ij":"ij","œ":"oe","":"st","":"a","":"e","":"i","":"j","":"o","":"r","":"u","":"v","":"x"}
};
String.prototype.latinise = function() {
return this.replace(/[^A-Za-z0-9]/g, function(x) { return Latinise.map[x] || x; });
};
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