Commit cd0ae854 authored by Clement Ho's avatar Clement Ho

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

# Conflicts:
#   app/assets/javascripts/lib/utils/text_utility.js
parents 421acc9a 2f74b1d3
...@@ -241,7 +241,7 @@ linters: ...@@ -241,7 +241,7 @@ linters:
# Numeric values should not contain unnecessary fractional portions. # Numeric values should not contain unnecessary fractional portions.
UnnecessaryMantissa: UnnecessaryMantissa:
enabled: false enabled: true
# Do not use parent selector references (&) when they would otherwise # Do not use parent selector references (&) when they would otherwise
# be unnecessary. # be unnecessary.
......
...@@ -2,6 +2,16 @@ ...@@ -2,6 +2,16 @@
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.4 (2017-11-14)
### Fixed (4 changes)
- Don't try to create fork network memberships for forks with a missing source. !15366
- Formats bytes to human reabale number in registry table.
- Prevent error when authorizing an admin-created OAauth application without a set owner.
- Prevents position update for image diff notes.
## 10.1.3 (2017-11-10) ## 10.1.3 (2017-11-10)
- [SECURITY] Prevent OAuth phishing attack by presenting detailed wording about app to user during authorization. - [SECURITY] Prevent OAuth phishing attack by presenting detailed wording about app to user during authorization.
......
...@@ -398,7 +398,7 @@ group :ed25519 do ...@@ -398,7 +398,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.51.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.52.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -274,7 +274,7 @@ GEM ...@@ -274,7 +274,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.51.0) gitaly-proto (0.52.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -1026,7 +1026,7 @@ DEPENDENCIES ...@@ -1026,7 +1026,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.51.0) gitaly-proto (~> 0.52.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2) gitlab-markup (~> 1.6.2)
...@@ -1185,4 +1185,4 @@ DEPENDENCIES ...@@ -1185,4 +1185,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.15.4 1.16.0
app/assets/images/emoji.png

1.16 MB | W: | H:

app/assets/images/emoji.png

1.16 MB | W: | H:

app/assets/images/emoji.png
app/assets/images/emoji.png
app/assets/images/emoji.png
app/assets/images/emoji.png
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/emoji/mrs_claus.png

2.15 KB | W: | H:

app/assets/images/emoji/mrs_claus.png

3.26 KB | W: | H:

app/assets/images/emoji/mrs_claus.png
app/assets/images/emoji/mrs_claus.png
app/assets/images/emoji/mrs_claus.png
app/assets/images/emoji/mrs_claus.png
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/emoji@2x.png

2.84 MB | W: | H:

app/assets/images/emoji@2x.png

2.84 MB | W: | H:

app/assets/images/emoji@2x.png
app/assets/images/emoji@2x.png
app/assets/images/emoji@2x.png
app/assets/images/emoji@2x.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -20,15 +20,15 @@ import groupsSelect from './groups_select'; ...@@ -20,15 +20,15 @@ import groupsSelect from './groups_select';
import NamespaceSelect from './namespace_select'; import NamespaceSelect from './namespace_select';
/* global NewCommitForm */ /* global NewCommitForm */
/* global NewBranchForm */ /* global NewBranchForm */
/* global Project */ import Project from './project';
/* global ProjectAvatar */ import projectAvatar from './project_avatar';
/* global MergeRequest */ /* global MergeRequest */
/* global Compare */ /* global Compare */
/* global CompareAutocomplete */ /* global CompareAutocomplete */
/* global ProjectFindFile */ /* global ProjectFindFile */
/* global ProjectNew */ /* global ProjectNew */
/* global ProjectShow */ /* global ProjectShow */
/* global ProjectImport */ import projectImport from './project_import';
import Labels from './labels'; import Labels from './labels';
import LabelManager from './label_manager'; import LabelManager from './label_manager';
/* global Sidebar */ /* global Sidebar */
...@@ -378,7 +378,7 @@ import Diff from './diff'; ...@@ -378,7 +378,7 @@ import Diff from './diff';
initSettingsPanels(); initSettingsPanels();
break; break;
case 'projects:imports:show': case 'projects:imports:show':
new ProjectImport(); projectImport();
break; break;
case 'projects:pipelines:new': case 'projects:pipelines:new':
new NewBranchForm($('.js-new-pipeline-form')); new NewBranchForm($('.js-new-pipeline-form'));
...@@ -604,7 +604,7 @@ import Diff from './diff'; ...@@ -604,7 +604,7 @@ import Diff from './diff';
break; break;
case 'projects': case 'projects':
new Project(); new Project();
new ProjectAvatar(); projectAvatar();
switch (path[1]) { switch (path[1]) {
case 'compare': case 'compare':
new CompareAutocomplete(); new CompareAutocomplete();
......
...@@ -7,6 +7,17 @@ function isFlagEmoji(emojiUnicode) { ...@@ -7,6 +7,17 @@ function isFlagEmoji(emojiUnicode) {
return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint; return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint;
} }
// Tested on mac OS 10.12.6 and Windows 10 FCU, it renders as two separate characters
const baseFlagCodePoint = 127987; // parseInt('1F3F3', 16)
const rainbowCodePoint = 127752; // parseInt('1F308', 16)
function isRainbowFlagEmoji(emojiUnicode) {
const characters = Array.from(emojiUnicode);
// Length 4 because flags are made of 2 characters which are surrogate pairs
return emojiUnicode.length === 4 &&
characters[0].codePointAt(0) === baseFlagCodePoint &&
characters[1].codePointAt(0) === rainbowCodePoint;
}
// Chrome <57 renders keycaps oddly // Chrome <57 renders keycaps oddly
// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294 // See https://bugs.chromium.org/p/chromium/issues/detail?id=632294
// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png // Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png
...@@ -57,9 +68,11 @@ function isPersonZwjEmoji(emojiUnicode) { ...@@ -57,9 +68,11 @@ function isPersonZwjEmoji(emojiUnicode) {
// in `isEmojiUnicodeSupported` logic // in `isEmojiUnicodeSupported` logic
function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) { function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isFlagResult = isFlagEmoji(emojiUnicode); const isFlagResult = isFlagEmoji(emojiUnicode);
const isRainbowFlagResult = isRainbowFlagEmoji(emojiUnicode);
return ( return (
(unicodeSupportMap.flag && isFlagResult) || (unicodeSupportMap.flag && isFlagResult) ||
!isFlagResult (unicodeSupportMap.rainbowFlag && isRainbowFlagResult) ||
(!isFlagResult && !isRainbowFlagResult)
); );
} }
...@@ -113,6 +126,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe ...@@ -113,6 +126,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe
export { export {
isEmojiUnicodeSupported as default, isEmojiUnicodeSupported as default,
isFlagEmoji, isFlagEmoji,
isRainbowFlagEmoji,
isKeycapEmoji, isKeycapEmoji,
isSkinToneComboEmoji, isSkinToneComboEmoji,
isHorceRacingSkinToneComboEmoji, isHorceRacingSkinToneComboEmoji,
......
import AccessorUtilities from '../../lib/utils/accessor'; import AccessorUtilities from '../../lib/utils/accessor';
const GL_EMOJI_VERSION = '0.2.0';
const unicodeSupportTestMap = { const unicodeSupportTestMap = {
// man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/ // man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
// occupationZwj: '\u{1F468}\u{200D}\u{1F393}', // occupationZwj: '\u{1F468}\u{200D}\u{1F393}',
...@@ -13,6 +15,7 @@ const unicodeSupportTestMap = { ...@@ -13,6 +15,7 @@ const unicodeSupportTestMap = {
horseRacing: '\u{1F3C7}\u{1F3FF}', horseRacing: '\u{1F3C7}\u{1F3FF}',
// US flag, http://emojipedia.org/flags/ // US flag, http://emojipedia.org/flags/
flag: '\u{1F1FA}\u{1F1F8}', flag: '\u{1F1FA}\u{1F1F8}',
rainbowFlag: '\u{1F3F3}\u{1F308}',
// http://emojipedia.org/modifiers/ // http://emojipedia.org/modifiers/
skinToneModifier: [ skinToneModifier: [
// spy_tone5 // spy_tone5
...@@ -141,23 +144,31 @@ function generateUnicodeSupportMap(testMap) { ...@@ -141,23 +144,31 @@ function generateUnicodeSupportMap(testMap) {
} }
export default function getUnicodeSupportMap() { export default function getUnicodeSupportMap() {
let unicodeSupportMap;
let userAgentFromCache;
const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
if (isLocalStorageAvailable) userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent'); let glEmojiVersionFromCache;
let userAgentFromCache;
if (isLocalStorageAvailable) {
glEmojiVersionFromCache = window.localStorage.getItem('gl-emoji-version');
userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
}
let unicodeSupportMap;
try { try {
unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map')); unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
} catch (err) { } catch (err) {
// swallow // swallow
} }
if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) { if (
!unicodeSupportMap ||
glEmojiVersionFromCache !== GL_EMOJI_VERSION ||
userAgentFromCache !== navigator.userAgent
) {
unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap); unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap);
if (isLocalStorageAvailable) { if (isLocalStorageAvailable) {
window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION);
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent); window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap)); window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
} }
......
...@@ -309,6 +309,42 @@ export const setParamInURL = (param, value) => { ...@@ -309,6 +309,42 @@ export const setParamInURL = (param, value) => {
return search; return search;
}; };
/**
* Given a string of query parameters creates an object.
*
* @example
* `scope=all&page=2` -> { scope: 'all', page: '2'}
* `scope=all` -> { scope: 'all' }
* ``-> {}
* @param {String} query
* @returns {Object}
*/
export const parseQueryStringIntoObject = (query = '') => {
if (query === '') return {};
return query
.split('&')
.reduce((acc, element) => {
const val = element.split('=');
Object.assign(acc, {
[val[0]]: decodeURIComponent(val[1]),
});
return acc;
}, {});
};
export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname);
/**
* Based on the current location and the string parameters provided
* creates a new entry in the history without reloading the page.
*
* @param {String} param
*/
export const historyPushState = (newUrl) => {
window.history.pushState({}, document.title, newUrl);
};
/** /**
* Converts permission provided as strings to booleans. * Converts permission provided as strings to booleans.
* *
......
...@@ -52,3 +52,31 @@ export function bytesToKiB(number) { ...@@ -52,3 +52,31 @@ export function bytesToKiB(number) {
export function bytesToMiB(number) { export function bytesToMiB(number) {
return number / (BYTES_IN_KIB * BYTES_IN_KIB); return number / (BYTES_IN_KIB * BYTES_IN_KIB);
} }
/**
* Utility function that calculates GiB of the given bytes.
* @param {Number} number
* @returns {Number}
*/
export function bytesToGiB(number) {
return number / (BYTES_IN_KIB * BYTES_IN_KIB * BYTES_IN_KIB);
}
/**
* Port of rails number_to_human_size
* Formats the bytes in number into a more understandable
* representation (e.g., giving it 1500 yields 1.5 KB).
*
* @param {Number} size
* @returns {String}
*/
export function numberToHumanSize(size) {
if (size < BYTES_IN_KIB) {
return `${size} bytes`;
} else if (size < BYTES_IN_KIB * BYTES_IN_KIB) {
return `${bytesToKiB(size).toFixed(2)} KiB`;
} else if (size < BYTES_IN_KIB * BYTES_IN_KIB * BYTES_IN_KIB) {
return `${bytesToMiB(size).toFixed(2)} MiB`;
}
return `${bytesToGiB(size).toFixed(2)} GiB`;
}
...@@ -60,7 +60,6 @@ export default class Poll { ...@@ -60,7 +60,6 @@ export default class Poll {
checkConditions(response) { checkConditions(response) {
const headers = normalizeHeaders(response.headers); const headers = normalizeHeaders(response.headers);
const pollInterval = parseInt(headers[this.intervalHeader], 10); const pollInterval = parseInt(headers[this.intervalHeader], 10);
if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) { if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
this.timeoutID = setTimeout(() => { this.timeoutID = setTimeout(() => {
this.makeRequest(); this.makeRequest();
...@@ -102,7 +101,12 @@ export default class Poll { ...@@ -102,7 +101,12 @@ export default class Poll {
/** /**
* Restarts polling after it has been stoped * Restarts polling after it has been stoped
*/ */
restart() { restart(options) {
// update data
if (options && options.data) {
this.options.data = options.data;
}
this.canPoll = true; this.canPoll = true;
this.makeRequest(); this.makeRequest();
} }
......
...@@ -100,6 +100,10 @@ export function visitUrl(url, external = false) { ...@@ -100,6 +100,10 @@ export function visitUrl(url, external = false) {
} }
} }
export function redirectTo(url) {
return window.location.assign(url);
}
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.utils = { window.gl.utils = {
...(window.gl.utils || {}), ...(window.gl.utils || {}),
......
...@@ -69,8 +69,6 @@ import './notifications_dropdown'; ...@@ -69,8 +69,6 @@ import './notifications_dropdown';
import './notifications_form'; import './notifications_form';
import './pager'; import './pager';
import './preview_markdown'; import './preview_markdown';
import './project';
import './project_avatar';
import './project_find_file'; import './project_find_file';
import './project_import'; import './project_import';
import './project_label_subscription'; import './project_label_subscription';
......
...@@ -5,7 +5,6 @@ export default class Members { ...@@ -5,7 +5,6 @@ export default class Members {
} }
addListeners() { addListeners() {
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
$('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this)); $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this)); $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
...@@ -33,17 +32,6 @@ export default class Members { ...@@ -33,17 +32,6 @@ export default class Members {
}); });
}); });
} }
// eslint-disable-next-line class-methods-use-this
removeRow(e) {
const $target = $(e.target);
if ($target.hasClass('btn-remove')) {
$target.closest('.member')
.fadeOut(function fadeOutMemberRow() {
$(this).remove();
});
}
}
formSubmit(e, $el = null) { formSubmit(e, $el = null) {
const $this = e ? $(e.currentTarget) : $el; const $this = e ? $(e.currentTarget) : $el;
......
...@@ -2,16 +2,8 @@ ...@@ -2,16 +2,8 @@
export default { export default {
name: 'PipelineNavigationTabs', name: 'PipelineNavigationTabs',
props: { props: {
scope: { tabs: {
type: String, type: Array,
required: true,
},
count: {
type: Object,
required: true,
},
paths: {
type: Object,
required: true, required: true,
}, },
}, },
...@@ -23,68 +15,37 @@ ...@@ -23,68 +15,37 @@
// 0 is valid in a badge, but evaluates to false, we need to check for undefined // 0 is valid in a badge, but evaluates to false, we need to check for undefined
return count !== undefined; return count !== undefined;
}, },
onTabClick(tab) {
this.$emit('onChangeTab', tab.scope);
},
}, },
}; };
</script> </script>
<template> <template>
<ul class="nav-links scrolling-tabs"> <ul class="nav-links scrolling-tabs">
<li <li
class="js-pipelines-tab-all" v-for="(tab, i) in tabs"
:class="{ active: scope === 'all'}"> :key="i"
<a :href="paths.allPath"> :class="{
All active: tab.isActive,
<span }"
v-if="shouldRenderBadge(count.all)" >
class="badge js-totalbuilds-count"> <a
{{count.all}} role="button"
</span> @click="onTabClick(tab)"
</a> :class="`js-pipelines-tab-${tab.scope}`"
</li> >
<li {{ tab.name }}
class="js-pipelines-tab-pending"
:class="{ active: scope === 'pending'}">
<a :href="paths.pendingPath">
Pending
<span
v-if="shouldRenderBadge(count.pending)"
class="badge">
{{count.pending}}
</span>
</a>
</li>
<li
class="js-pipelines-tab-running"
:class="{ active: scope === 'running'}">
<a :href="paths.runningPath">
Running
<span
v-if="shouldRenderBadge(count.running)"
class="badge">
{{count.running}}
</span>
</a>
</li>
<li
class="js-pipelines-tab-finished"
:class="{ active: scope === 'finished'}">
<a :href="paths.finishedPath">
Finished
<span <span
v-if="shouldRenderBadge(count.finished)" v-if="shouldRenderBadge(tab.count)"
class="badge"> class="badge"
{{count.finished}} >
{{tab.count}}
</span> </span>
</a> </a>
</li> </li>
<li
class="js-pipelines-tab-branches"
:class="{ active: scope === 'branches'}">
<a :href="paths.branchesPath">Branches</a>
</li>
<li
class="js-pipelines-tab-tags"
:class="{ active: scope === 'tags'}">
<a :href="paths.tagsPath">Tags</a>
</li>
</ul> </ul>
</template> </template>
<script> <script>
import _ from 'underscore';
import PipelinesService from '../services/pipelines_service'; import PipelinesService from '../services/pipelines_service';
import pipelinesMixin from '../mixins/pipelines'; import pipelinesMixin from '../mixins/pipelines';
import tablePagination from '../../vue_shared/components/table_pagination.vue'; import tablePagination from '../../vue_shared/components/table_pagination.vue';
import navigationTabs from './navigation_tabs.vue'; import navigationTabs from './navigation_tabs.vue';
import navigationControls from './nav_controls.vue'; import navigationControls from './nav_controls.vue';
import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils'; import {
convertPermissionToBoolean,
getParameterByName,
historyPushState,
buildUrlWithCurrentLocation,
parseQueryStringIntoObject,
} from '../../lib/utils/common_utils';
export default { export default {
props: { props: {
...@@ -41,27 +48,18 @@ ...@@ -41,27 +48,18 @@
autoDevopsPath: pipelinesData.helpAutoDevopsPath, autoDevopsPath: pipelinesData.helpAutoDevopsPath,
newPipelinePath: pipelinesData.newPipelinePath, newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline, canCreatePipeline: pipelinesData.canCreatePipeline,
allPath: pipelinesData.allPath,
pendingPath: pipelinesData.pendingPath,
runningPath: pipelinesData.runningPath,
finishedPath: pipelinesData.finishedPath,
branchesPath: pipelinesData.branchesPath,
tagsPath: pipelinesData.tagsPath,
hasCi: pipelinesData.hasCi, hasCi: pipelinesData.hasCi,
ciLintPath: pipelinesData.ciLintPath, ciLintPath: pipelinesData.ciLintPath,
state: this.store.state, state: this.store.state,
apiScope: 'all', scope: getParameterByName('scope') || 'all',
pagenum: 1, page: getParameterByName('page') || '1',
requestData: {},
}; };
}, },
computed: { computed: {
canCreatePipelineParsed() { canCreatePipelineParsed() {
return convertPermissionToBoolean(this.canCreatePipeline); return convertPermissionToBoolean(this.canCreatePipeline);
}, },
scope() {
const scope = getParameterByName('scope');
return scope === null ? 'all' : scope;
},
/** /**
* The empty state should only be rendered when the request is made to fetch all pipelines * The empty state should only be rendered when the request is made to fetch all pipelines
...@@ -106,47 +104,113 @@ ...@@ -106,47 +104,113 @@
hasCiEnabled() { hasCiEnabled() {
return this.hasCi !== undefined; return this.hasCi !== undefined;
}, },
paths() {
return { tabs() {
allPath: this.allPath, const { count } = this.state;
pendingPath: this.pendingPath, return [
finishedPath: this.finishedPath, {
runningPath: this.runningPath, name: 'All',
branchesPath: this.branchesPath, scope: 'all',
tagsPath: this.tagsPath, count: count.all,
}; isActive: this.scope === 'all',
},
{
name: 'Pending',
scope: 'pending',
count: count.pending,
isActive: this.scope === 'pending',
},
{
name: 'Running',
scope: 'running',
count: count.running,
isActive: this.scope === 'running',
},
{
name: 'Finished',
scope: 'finished',
count: count.finished,
isActive: this.scope === 'finished',
}, },
pageParameter() { {
return getParameterByName('page') || this.pagenum; name: 'Branches',
scope: 'branches',
isActive: this.scope === 'branches',
}, },
scopeParameter() { {
return getParameterByName('scope') || this.apiScope; name: 'Tags',
scope: 'tags',
isActive: this.scope === 'tags',
},
];
}, },
}, },
created() { created() {
this.service = new PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
this.requestData = { page: this.pageParameter, scope: this.scopeParameter }; this.requestData = { page: this.page, scope: this.scope };
}, },
methods: { methods: {
/**
* Will change the page number and update the URL.
*
* @param {Number} pageNumber desired page to go to.
*/
change(pageNumber) {
const param = setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
},
successCallback(resp) { successCallback(resp) {
return resp.json().then((response) => { return resp.json().then((response) => {
// Because we are polling & the user is interacting verify if the response received
// matches the last request made
if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) {
this.store.storeCount(response.count); this.store.storeCount(response.count);
this.store.storePagination(resp.headers); this.store.storePagination(resp.headers);
this.setCommonData(response.pipelines); this.setCommonData(response.pipelines);
}
}); });
}, },
/**
* Handles URL and query parameter changes.
* When the user uses the pagination or the tabs,
* - update URL
* - Make API request to the server with new parameters
* - Update the polling function
* - Update the internal state
*/
updateContent(parameters) {
// stop polling
this.poll.stop();
const queryString = Object.keys(parameters).map((parameter) => {
const value = parameters[parameter];
// update internal state for UI
this[parameter] = value;
return `${parameter}=${encodeURIComponent(value)}`;
}).join('&');
// update polling parameters
this.requestData = parameters;
historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
this.isLoading = true;
// fetch new data
return this.service.getPipelines(this.requestData)
.then((response) => {
this.isLoading = false;
this.successCallback(response);
// restart polling
this.poll.restart({ data: this.requestData });
})
.catch(() => {
this.isLoading = false;
this.errorCallback();
// restart polling
this.poll.restart();
});
},
onChangeTab(scope) {
this.updateContent({ scope, page: '1' });
},
onChangePage(page) {
/* URLS parameters are strings, we need to parse to match types */
this.updateContent({ scope: this.scope, page: Number(page).toString() });
},
}, },
}; };
</script> </script>
...@@ -154,7 +218,7 @@ ...@@ -154,7 +218,7 @@
<div class="pipelines-container"> <div class="pipelines-container">
<div <div
class="top-area scrolling-tabs-container inner-page-scroll-tabs" class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState"> v-if="!shouldRenderEmptyState">
<div class="fade-left"> <div class="fade-left">
<i <i
class="fa fa-angle-left" class="fa fa-angle-left"
...@@ -167,17 +231,17 @@ ...@@ -167,17 +231,17 @@
aria-hidden="true"> aria-hidden="true">
</i> </i>
</div> </div>
<navigation-tabs <navigation-tabs
:scope="scope" :tabs="tabs"
:count="state.count" @onChangeTab="onChangeTab"
:paths="paths"
/> />
<navigation-controls <navigation-controls
:new-pipeline-path="newPipelinePath" :new-pipeline-path="newPipelinePath"
:has-ci-enabled="hasCiEnabled" :has-ci-enabled="hasCiEnabled"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:ciLintPath="ciLintPath" :ci-lint-path="ciLintPath"
:can-create-pipeline="canCreatePipelineParsed " :can-create-pipeline="canCreatePipelineParsed "
/> />
</div> </div>
...@@ -188,6 +252,7 @@ ...@@ -188,6 +252,7 @@
label="Loading Pipelines" label="Loading Pipelines"
size="3" size="3"
v-if="isLoading" v-if="isLoading"
class="prepend-top-20"
/> />
<empty-state <empty-state
...@@ -221,8 +286,8 @@ ...@@ -221,8 +286,8 @@
<table-pagination <table-pagination
v-if="shouldRenderPagination" v-if="shouldRenderPagination"
:change="change" :change="onChangePage"
:pageInfo="state.pageInfo" :page-info="state.pageInfo"
/> />
</div> </div>
</div> </div>
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
/* global ProjectSelect */ /* global ProjectSelect */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
(function() { export default class Project {
this.Project = (function() { constructor() {
function Project() {
const $cloneOptions = $('ul.clone-options-dropdown'); const $cloneOptions = $('ul.clone-options-dropdown');
const $projectCloneField = $('#project_clone'); const $projectCloneField = $('#project_clone');
const $cloneBtnText = $('a.clone-dropdown-btn span'); const $cloneBtnText = $('a.clone-dropdown-btn span');
...@@ -29,7 +28,7 @@ import Cookies from 'js-cookie'; ...@@ -29,7 +28,7 @@ import Cookies from 'js-cookie';
return $('.clone').text(url); return $('.clone').text(url);
}); });
// Ref switcher // Ref switcher
this.initRefSwitcher(); Project.initRefSwitcher();
$('.project-refs-select').on('change', function() { $('.project-refs-select').on('change', function() {
return $(this).parents('form').submit(); return $(this).parents('form').submit();
}); });
...@@ -43,23 +42,19 @@ import Cookies from 'js-cookie'; ...@@ -43,23 +42,19 @@ import Cookies from 'js-cookie';
$(this).parents('.no-password-message').remove(); $(this).parents('.no-password-message').remove();
return e.preventDefault(); return e.preventDefault();
}); });
this.projectSelectDropdown(); Project.projectSelectDropdown();
} }
Project.prototype.projectSelectDropdown = function() { static projectSelectDropdown () {
new ProjectSelect(); new ProjectSelect();
$('.project-item-select').on('click', (function(_this) { $('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val()));
return function(e) { }
return _this.changeProject($(e.currentTarget).val());
}; static changeProject(url) {
})(this));
};
Project.prototype.changeProject = function(url) {
return window.location = url; return window.location = url;
}; }
Project.prototype.initRefSwitcher = function() { static initRefSwitcher() {
var refListItem = document.createElement('li'); var refListItem = document.createElement('li');
var refLink = document.createElement('a'); var refLink = document.createElement('a');
...@@ -75,9 +70,9 @@ import Cookies from 'js-cookie'; ...@@ -75,9 +70,9 @@ import Cookies from 'js-cookie';
url: $dropdown.data('refs-url'), url: $dropdown.data('refs-url'),
data: { data: {
ref: $dropdown.data('ref'), ref: $dropdown.data('ref'),
search: term search: term,
}, },
dataType: "json" dataType: 'json',
}).done(function(refs) { }).done(function(refs) {
return callback(refs); return callback(refs);
}); });
...@@ -129,11 +124,8 @@ import Cookies from 'js-cookie'; ...@@ -129,11 +124,8 @@ import Cookies from 'js-cookie';
gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`); gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
} }
} }
} },
}); });
}); });
}; }
}
return Project;
})();
}).call(window);
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */ export default function projectAvatar() {
(function() { $('.js-choose-project-avatar-button').bind('click', function onClickAvatar() {
this.ProjectAvatar = (function() { const form = $(this).closest('form');
function ProjectAvatar() {
$('.js-choose-project-avatar-button').bind('click', function() {
var form;
form = $(this).closest('form');
return form.find('.js-project-avatar-input').click(); return form.find('.js-project-avatar-input').click();
}); });
$('.js-project-avatar-input').bind('change', function() {
var filename, form; $('.js-project-avatar-input').bind('change', function onClickAvatarInput() {
form = $(this).closest('form'); const form = $(this).closest('form');
filename = $(this).val().replace(/^.*[\\\/]/, ''); // eslint-disable-next-line no-useless-escape
const filename = $(this).val().replace(/^.*[\\\/]/, '');
return form.find('.js-avatar-filename').text(filename); return form.find('.js-avatar-filename').text(filename);
}); });
} }
return ProjectAvatar;
})();
}).call(window);
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */ import { visitUrl } from './lib/utils/url_utility';
(function() { export default function projectImport() {
this.ProjectImport = (function() { setTimeout(() => {
function ProjectImport() { visitUrl(location.href);
setTimeout(function() {
return gl.utils.visitUrl(location.href);
}, 5000); }, 5000);
} }
return ProjectImport;
})();
}).call(window);
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago'; import timeagoMixin from '../../vue_shared/mixins/timeago';
import { errorMessages, errorMessagesTypes } from '../constants'; import { errorMessages, errorMessagesTypes } from '../constants';
import { numberToHumanSize } from '../../lib/utils/number_utils';
export default { export default {
props: { props: {
...@@ -41,6 +42,10 @@ ...@@ -41,6 +42,10 @@
return item.layers ? n__('%d layer', '%d layers', item.layers) : ''; return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
}, },
formatSize(size) {
return numberToHumanSize(size);
},
handleDeleteRegistry(registry) { handleDeleteRegistry(registry) {
this.deleteRegistry(registry) this.deleteRegistry(registry)
.then(() => this.fetchList({ repo: this.repo })) .then(() => this.fetchList({ repo: this.repo }))
...@@ -97,7 +102,7 @@ ...@@ -97,7 +102,7 @@
</span> </span>
</td> </td>
<td> <td>
{{item.size}} {{formatSize(item.size)}}
<template v-if="item.size && item.layers"> <template v-if="item.size && item.layers">
&middot; &middot;
</template> </template>
......
...@@ -61,7 +61,7 @@ export default { ...@@ -61,7 +61,7 @@ export default {
return this.mr.hasCI; return this.mr.hasCI;
}, },
shouldRenderRelatedLinks() { shouldRenderRelatedLinks() {
return this.mr.relatedLinks; return !!this.mr.relatedLinks;
}, },
shouldRenderDeployments() { shouldRenderDeployments() {
return this.mr.deployments.length; return this.mr.deployments.length;
......
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
@import "framework/modal"; @import "framework/modal";
@import "framework/pagination"; @import "framework/pagination";
@import "framework/panels"; @import "framework/panels";
@import "framework/popup";
@import "framework/secondary-navigation-elements"; @import "framework/secondary-navigation-elements";
@import "framework/selects"; @import "framework/selects";
@import "framework/sidebar"; @import "framework/sidebar";
......
...@@ -7,17 +7,21 @@ ...@@ -7,17 +7,21 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
padding-bottom: 25px; padding-bottom: 25px;
border: 1px solid $border-color;
border-radius: $border-radius-default; border-radius: $border-radius-default;
} }
} }
.blank-state { .blank-state-row {
padding-top: 20px; display: flex;
padding-bottom: 20px; flex-wrap: wrap;
justify-content: space-around;
height: 100%;
}
.blank-state-welcome {
text-align: center; text-align: center;
padding: 20px 0 40px;
&.blank-state-welcome {
.blank-state-welcome-title { .blank-state-welcome-title {
font-size: 24px; font-size: 24px;
} }
...@@ -25,11 +29,45 @@ ...@@ -25,11 +29,45 @@
.blank-state-text { .blank-state-text {
margin-bottom: 0; margin-bottom: 0;
} }
}
.blank-state-link {
display: block;
color: $gl-text-color;
flex: 0 0 100%;
margin-bottom: 15px;
@media (min-width: $screen-sm-min) {
flex: 0 0 49%;
&:nth-child(odd) {
margin-right: 5px;
}
&:nth-child(even) {
margin-left: 5px;
}
} }
.blank-state-icon { &:hover {
padding-bottom: 20px; background-color: $gray-light;
text-decoration: none;
color: $gl-text-color;
}
}
.blank-state {
padding: 20px;
border: 1px solid $border-color;
border-radius: $border-radius-default;
@media (min-width: $screen-sm-min) {
display: flex;
align-items: center;
padding: 50px 30px;
}
.blank-state-icon {
svg { svg {
display: block; display: block;
margin: auto; margin: auto;
...@@ -38,13 +76,17 @@ ...@@ -38,13 +76,17 @@
.blank-state-title { .blank-state-title {
margin-top: 0; margin-top: 0;
margin-bottom: 10px;
font-size: 18px; font-size: 18px;
} }
.blank-state-text { .blank-state-body {
max-width: $container-text-max-width; @media (max-width: $screen-xs-max) {
margin: 0 auto $gl-padding; text-align: center;
font-size: 14px; margin-top: 20px;
}
@media (min-width: $screen-sm-min) {
padding-left: 20px;
}
} }
} }
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -101,13 +101,13 @@ ...@@ -101,13 +101,13 @@
@for $i from 0 through 5 { @for $i from 0 through 5 {
.legend-box-#{$i} { .legend-box-#{$i} {
background-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%); background-color: mix($blame-cyan, $blame-blue, $i / 5 * 100%);
} }
} }
@for $i from 1 through 4 { @for $i from 1 through 4 {
.legend-box-#{$i + 5} { .legend-box-#{$i + 5} {
background-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%); background-color: mix($blame-gray, $blame-cyan, $i / 4 * 100%);
} }
} }
} }
...@@ -200,13 +200,13 @@ ...@@ -200,13 +200,13 @@
@for $i from 0 through 5 { @for $i from 0 through 5 {
td.blame-commit-age-#{$i} { td.blame-commit-age-#{$i} {
border-left-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%); border-left-color: mix($blame-cyan, $blame-blue, $i / 5 * 100%);
} }
} }
@for $i from 1 through 4 { @for $i from 1 through 4 {
td.blame-commit-age-#{$i + 5} { td.blame-commit-age-#{$i + 5} {
border-left-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%); border-left-color: mix($blame-gray, $blame-cyan, $i / 4 * 100%);
} }
} }
} }
......
...@@ -129,7 +129,7 @@ ...@@ -129,7 +129,7 @@
margin: 5px 2px 5px -8px; margin: 5px 2px 5px -8px;
border-radius: $border-radius-default; border-radius: $border-radius-default;
svg { .tanuki-logo {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
margin-right: 8px; margin-right: 8px;
} }
......
...@@ -180,3 +180,31 @@ ...@@ -180,3 +180,31 @@
display: none; display: none;
} }
} }
@mixin triangle($color, $border-color, $size, $border-size) {
&::before,
&::after {
bottom: 100%;
left: 50%;
border: solid transparent;
content: '';
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
&::before {
border-color: transparent;
border-bottom-color: $border-color;
border-width: ($size + $border-size);
margin-left: -($size + $border-size);
}
&::after {
border-color: transparent;
border-bottom-color: $color;
border-width: $size;
margin-left: -$size;
}
}
.popup {
@include triangle(
$gray-lighter,
$gray-darker,
$popup-triangle-size,
$popup-triangle-border-size
);
padding: $gl-padding;
background-color: $gray-lighter;
border: 1px solid $gray-darker;
border-radius: $border-radius-default;
box-shadow: 0 5px 8px $popup-box-shadow-color;
position: relative;
}
...@@ -163,7 +163,7 @@ $gl-text-color: #2e2e2e; ...@@ -163,7 +163,7 @@ $gl-text-color: #2e2e2e;
$gl-text-color-secondary: #707070; $gl-text-color-secondary: #707070;
$gl-text-color-tertiary: #949494; $gl-text-color-tertiary: #949494;
$gl-text-color-quaternary: #d6d6d6; $gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: rgba(255, 255, 255, 1.0); $gl-text-color-inverted: rgba(255, 255, 255, 1);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, .85); $gl-text-color-secondary-inverted: rgba(255, 255, 255, .85);
$gl-text-green: $green-600; $gl-text-green: $green-600;
$gl-text-green-hover: $green-700; $gl-text-green-hover: $green-700;
...@@ -486,8 +486,8 @@ $callout-success-color: $green-700; ...@@ -486,8 +486,8 @@ $callout-success-color: $green-700;
/* /*
* Commit Page * Commit Page
*/ */
$commit-max-width-marker-color: rgba(0, 0, 0, 0.0); $commit-max-width-marker-color: rgba(0, 0, 0, 0);
$commit-message-text-area-bg: rgba(0, 0, 0, 0.0); $commit-message-text-area-bg: rgba(0, 0, 0, 0);
/* /*
* Common * Common
...@@ -719,3 +719,10 @@ Image Commenting cursor ...@@ -719,3 +719,10 @@ Image Commenting cursor
*/ */
$image-comment-cursor-left-offset: 12; $image-comment-cursor-left-offset: 12;
$image-comment-cursor-top-offset: 30; $image-comment-cursor-top-offset: 30;
/*
Popup
*/
$popup-triangle-size: 15px;
$popup-triangle-border-size: 1px;
$popup-box-shadow-color: rgba(90, 90, 90, 0.05);
...@@ -173,7 +173,7 @@ ...@@ -173,7 +173,7 @@
.prometheus-graph-overlay { .prometheus-graph-overlay {
fill: none; fill: none;
opacity: 0.0; opacity: 0;
pointer-events: all; pointer-events: all;
} }
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.diff-file .diff-content { .diff-file .diff-content {
tr.line_holder:hover > td .line_note_link { tr.line_holder:hover > td .line_note_link {
opacity: 1.0; opacity: 1;
filter: alpha(opacity = 100); filter: alpha(opacity = 100);
} }
} }
......
...@@ -125,7 +125,7 @@ ...@@ -125,7 +125,7 @@
color: $white-normal; color: $white-normal;
} }
&:hover { &:hover:not(.tree-truncated-warning) {
td { td {
background-color: $row-hover; background-color: $row-hover;
border-top: 1px solid $row-hover-border; border-top: 1px solid $row-hover-border;
...@@ -198,6 +198,11 @@ ...@@ -198,6 +198,11 @@
} }
} }
.tree-truncated-warning {
color: $orange-600;
background-color: $orange-100;
}
.tree-time-ago { .tree-time-ago {
min-width: 135px; min-width: 135px;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
......
...@@ -92,15 +92,7 @@ module LfsRequest ...@@ -92,15 +92,7 @@ module LfsRequest
end end
def storage_project def storage_project
@storage_project ||= begin @storage_project ||= project.lfs_storage_project
result = project
# TODO: Make this go to the fork_network root immeadiatly
# dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
result = result.fork_source while result.forked?
result
end
end end
def objects def objects
......
...@@ -4,7 +4,8 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -4,7 +4,8 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_read_build!, before_action :authorize_read_build!,
only: [:index, :show, :status, :raw, :trace] only: [:index, :show, :status, :raw, :trace]
before_action :authorize_update_build!, before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :cancel_all] except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase]
before_action :authorize_erase_build!, only: [:erase]
layout 'project' layout 'project'
...@@ -131,6 +132,10 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -131,6 +132,10 @@ class Projects::JobsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :update_build, build) return access_denied! unless can?(current_user, :update_build, build)
end end
def authorize_erase_build!
return access_denied! unless can?(current_user, :erase_build, build)
end
def build def build
@build ||= project.builds.find(params[:id]) @build ||= project.builds.find(params[:id])
.present(current_user: current_user) .present(current_user: current_user)
......
...@@ -36,6 +36,7 @@ class IssuableFinder ...@@ -36,6 +36,7 @@ class IssuableFinder
iids iids
label_name label_name
milestone_title milestone_title
my_reaction_emoji
non_archived non_archived
project_id project_id
scope scope
......
...@@ -4,7 +4,10 @@ module NamespacesHelper ...@@ -4,7 +4,10 @@ module NamespacesHelper
end end
def namespaces_options(selected = :current_user, display_path: false, extra_group: nil) def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
groups = current_user.owned_groups + current_user.masters_groups groups = current_user.manageable_groups
.joins(:route)
.includes(:route)
.order('routes.path')
users = [current_user.namespace] users = [current_user.namespace]
unless extra_group.nil? || extra_group.is_a?(Group) unless extra_group.nil? || extra_group.is_a?(Group)
......
module TreeHelper module TreeHelper
FILE_LIMIT = 1_000
# Sorts a repository's tree so that folders are before files and renders # Sorts a repository's tree so that folders are before files and renders
# their corresponding partials # their corresponding partials
# #
# contents - A Grit::Tree object for the current tree # tree - A `Tree` object for the current tree
def render_tree(tree) def render_tree(tree)
# Sort submodules and folders together by name ahead of files # Sort submodules and folders together by name ahead of files
folders, files, submodules = tree.trees, tree.blobs, tree.submodules folders, files, submodules = tree.trees, tree.blobs, tree.submodules
tree = "" tree = ''
items = (folders + submodules).sort_by(&:name) + files items = (folders + submodules).sort_by(&:name) + files
tree << render(partial: "projects/tree/tree_row", collection: items) if items.present?
if items.size > FILE_LIMIT
tree << render(partial: 'projects/tree/truncated_notice_tree_row',
locals: { limit: FILE_LIMIT, total: items.size })
items = items.take(FILE_LIMIT)
end
tree << render(partial: 'projects/tree/tree_row', collection: items) if items.present?
tree.html_safe tree.html_safe
end end
......
...@@ -192,6 +192,10 @@ module Ci ...@@ -192,6 +192,10 @@ module Ci
project.build_timeout project.build_timeout
end end
def triggered_by?(current_user)
user == current_user
end
# A slugified version of the build ref, suitable for inclusion in URLs and # A slugified version of the build ref, suitable for inclusion in URLs and
# domain names. Rules: # domain names. Rules:
# #
......
...@@ -66,8 +66,8 @@ module Ci ...@@ -66,8 +66,8 @@ module Ci
state_machine :status, initial: :created do state_machine :status, initial: :created do
event :enqueue do event :enqueue do
transition created: :pending transition [:created, :skipped] => :pending
transition [:success, :failed, :canceled, :skipped] => :running transition [:success, :failed, :canceled] => :running
end end
event :run do event :run do
......
...@@ -255,7 +255,7 @@ module Issuable ...@@ -255,7 +255,7 @@ module Issuable
participants(user).include?(user) participants(user).include?(user)
end end
def to_hook_data(user, old_labels: [], old_assignees: []) def to_hook_data(user, old_labels: [], old_assignees: [], old_total_time_spent: nil)
changes = previous_changes changes = previous_changes
if old_labels != labels if old_labels != labels
...@@ -270,6 +270,10 @@ module Issuable ...@@ -270,6 +270,10 @@ module Issuable
end end
end end
if old_total_time_spent != total_time_spent
changes[:total_time_spent] = [old_total_time_spent, total_time_spent]
end
Gitlab::HookData::IssuableBuilder.new(self).build(user: user, changes: changes) Gitlab::HookData::IssuableBuilder.new(self).build(user: user, changes: changes)
end end
......
...@@ -18,7 +18,8 @@ class DiffNote < Note ...@@ -18,7 +18,8 @@ class DiffNote < Note
validate :positions_complete validate :positions_complete
validate :verify_supported validate :verify_supported
before_validation :set_original_position, :update_position, on: :create before_validation :set_original_position, on: :create
before_validation :update_position, on: :create, if: :on_text?
before_validation :set_line_code before_validation :set_line_code
after_save :keep_around_commits after_save :keep_around_commits
......
...@@ -6,16 +6,8 @@ class LfsObject < ActiveRecord::Base ...@@ -6,16 +6,8 @@ class LfsObject < ActiveRecord::Base
mount_uploader :file, LfsObjectUploader mount_uploader :file, LfsObjectUploader
def storage_project(project)
if project && project.forked?
storage_project(project.forked_from_project)
else
project
end
end
def project_allowed_access?(project) def project_allowed_access?(project)
projects.exists?(storage_project(project).id) projects.exists?(project.lfs_storage_project.id)
end end
def self.destroy_unreferenced def self.destroy_unreferenced
......
...@@ -704,10 +704,6 @@ class Project < ActiveRecord::Base ...@@ -704,10 +704,6 @@ class Project < ActiveRecord::Base
import_type == 'gitea' import_type == 'gitea'
end end
def github_import?
import_type == 'github'
end
def check_limit def check_limit
unless creator.can_create_project? || namespace.kind == 'group' unless creator.can_create_project? || namespace.kind == 'group'
projects_limit = creator.projects_limit projects_limit = creator.projects_limit
...@@ -1047,6 +1043,18 @@ class Project < ActiveRecord::Base ...@@ -1047,6 +1043,18 @@ class Project < ActiveRecord::Base
forked_from_project || fork_network&.root_project forked_from_project || fork_network&.root_project
end end
def lfs_storage_project
@lfs_storage_project ||= begin
result = self
# TODO: Make this go to the fork_network root immeadiatly
# dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
result = result.fork_source while result&.forked?
result || self
end
end
def personal? def personal?
!group !group
end end
......
...@@ -25,7 +25,7 @@ class PrometheusService < MonitoringService ...@@ -25,7 +25,7 @@ class PrometheusService < MonitoringService
end end
def description def description
'Prometheus monitoring' s_('PrometheusService|Prometheus monitoring')
end end
def self.to_param def self.to_param
...@@ -38,8 +38,8 @@ class PrometheusService < MonitoringService ...@@ -38,8 +38,8 @@ class PrometheusService < MonitoringService
type: 'text', type: 'text',
name: 'api_url', name: 'api_url',
title: 'API URL', title: 'API URL',
placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/', placeholder: s_('PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/'),
help: 'By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.', help: s_('PrometheusService|By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.'),
required: true required: true
} }
] ]
......
...@@ -21,7 +21,7 @@ class ProjectWiki ...@@ -21,7 +21,7 @@ class ProjectWiki
end end
delegate :empty?, to: :pages delegate :empty?, to: :pages
delegate :repository_storage_path, to: :project delegate :repository_storage_path, :hashed_storage?, to: :project
def path def path
@project.path + '.wiki' @project.path + '.wiki'
......
...@@ -921,7 +921,16 @@ class User < ActiveRecord::Base ...@@ -921,7 +921,16 @@ class User < ActiveRecord::Base
end end
def manageable_namespaces def manageable_namespaces
@manageable_namespaces ||= [namespace] + owned_groups + masters_groups @manageable_namespaces ||= [namespace] + manageable_groups
end
def manageable_groups
union = Gitlab::SQL::Union.new([owned_groups.select(:id),
masters_groups.select(:id)])
arel_union = Arel::Nodes::SqlLiteral.new(union.to_sql)
owned_and_master_groups = Group.where(Group.arel_table[:id].in(arel_union))
Gitlab::GroupHierarchy.new(owned_and_master_groups).base_and_descendants
end end
def namespaces def namespaces
......
...@@ -10,6 +10,15 @@ module Ci ...@@ -10,6 +10,15 @@ module Ci
end end
end end
rule { protected_ref }.prevent :update_build condition(:owner_of_job) do
can?(:developer_access) && @subject.triggered_by?(@user)
end
rule { protected_ref }.policy do
prevent :update_build
prevent :erase_build
end
rule { can?(:master_access) | owner_of_job }.enable :erase_build
end end
end end
...@@ -6,7 +6,7 @@ class BuildDetailsEntity < JobEntity ...@@ -6,7 +6,7 @@ class BuildDetailsEntity < JobEntity
expose :pipeline, using: PipelineEntity expose :pipeline, using: PipelineEntity
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :update_build, project) } do |build| expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
erase_project_job_path(project, build) erase_project_job_path(project, build)
end end
......
...@@ -172,6 +172,7 @@ class IssuableBaseService < BaseService ...@@ -172,6 +172,7 @@ class IssuableBaseService < BaseService
old_labels = issuable.labels.to_a old_labels = issuable.labels.to_a
old_mentioned_users = issuable.mentioned_users.to_a old_mentioned_users = issuable.mentioned_users.to_a
old_assignees = issuable.assignees.to_a old_assignees = issuable.assignees.to_a
old_total_time_spent = issuable.total_time_spent
label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids) label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids) params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
...@@ -208,7 +209,12 @@ class IssuableBaseService < BaseService ...@@ -208,7 +209,12 @@ class IssuableBaseService < BaseService
invalidate_cache_counts(issuable, users: affected_assignees.compact) invalidate_cache_counts(issuable, users: affected_assignees.compact)
after_update(issuable) after_update(issuable)
issuable.create_new_cross_references!(current_user) issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update', old_labels: old_labels, old_assignees: old_assignees) execute_hooks(
issuable,
'update',
old_labels: old_labels,
old_assignees: old_assignees,
old_total_time_spent: old_total_time_spent)
issuable.update_project_counter_caches if update_project_counters issuable.update_project_counter_caches if update_project_counters
end end
......
module Issues module Issues
class BaseService < ::IssuableBaseService class BaseService < ::IssuableBaseService
def hook_data(issue, action, old_labels: [], old_assignees: []) def hook_data(issue, action, old_labels: [], old_assignees: [], old_total_time_spent: nil)
hook_data = issue.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees) hook_data = issue.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent)
hook_data[:object_attributes][:action] = action hook_data[:object_attributes][:action] = action
hook_data hook_data
...@@ -22,8 +22,8 @@ module Issues ...@@ -22,8 +22,8 @@ module Issues
issue, issue.project, current_user, old_assignees) issue, issue.project, current_user, old_assignees)
end end
def execute_hooks(issue, action = 'open', old_labels: [], old_assignees: []) def execute_hooks(issue, action = 'open', old_labels: [], old_assignees: [], old_total_time_spent: nil)
issue_data = hook_data(issue, action, old_labels: old_labels, old_assignees: old_assignees) issue_data = hook_data(issue, action, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent)
hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks
issue.project.execute_hooks(issue_data, hooks_scope) issue.project.execute_hooks(issue_data, hooks_scope)
issue.project.execute_services(issue_data, hooks_scope) issue.project.execute_services(issue_data, hooks_scope)
......
...@@ -18,8 +18,8 @@ module MergeRequests ...@@ -18,8 +18,8 @@ module MergeRequests
super if changed_title super if changed_title
end end
def hook_data(merge_request, action, old_rev: nil, old_labels: [], old_assignees: []) def hook_data(merge_request, action, old_rev: nil, old_labels: [], old_assignees: [], old_total_time_spent: nil)
hook_data = merge_request.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees) hook_data = merge_request.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent)
hook_data[:object_attributes][:action] = action hook_data[:object_attributes][:action] = action
if old_rev && !Gitlab::Git.blank_ref?(old_rev) if old_rev && !Gitlab::Git.blank_ref?(old_rev)
hook_data[:object_attributes][:oldrev] = old_rev hook_data[:object_attributes][:oldrev] = old_rev
...@@ -28,9 +28,9 @@ module MergeRequests ...@@ -28,9 +28,9 @@ module MergeRequests
hook_data hook_data
end end
def execute_hooks(merge_request, action = 'open', old_rev: nil, old_labels: [], old_assignees: []) def execute_hooks(merge_request, action = 'open', old_rev: nil, old_labels: [], old_assignees: [], old_total_time_spent: nil)
if merge_request.project if merge_request.project
merge_data = hook_data(merge_request, action, old_rev: old_rev, old_labels: old_labels, old_assignees: old_assignees) merge_data = hook_data(merge_request, action, old_rev: old_rev, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent)
merge_request.project.execute_hooks(merge_data, :merge_request_hooks) merge_request.project.execute_hooks(merge_data, :merge_request_hooks)
merge_request.project.execute_services(merge_data, :merge_request_hooks) merge_request.project.execute_services(merge_data, :merge_request_hooks)
end end
......
...@@ -11,13 +11,11 @@ module Projects ...@@ -11,13 +11,11 @@ module Projects
# supported by an importer class (`Gitlab::GithubImport::ParallelImporter` # supported by an importer class (`Gitlab::GithubImport::ParallelImporter`
# for example). # for example).
def async? def async?
return false unless has_importer? has_importer? && !!importer_class.try(:async?)
!!importer_class.try(:async?)
end end
def execute def execute
add_repository_to_project unless project.gitlab_project_import? add_repository_to_project
import_data import_data
...@@ -29,6 +27,14 @@ module Projects ...@@ -29,6 +27,14 @@ module Projects
private private
def add_repository_to_project def add_repository_to_project
if project.external_import? && !unknown_url?
raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url)
end
# We should skip the repository for a GitHub import or GitLab project import,
# because these importers fetch the project repositories for us.
return if has_importer? && importer_class.try(:imports_repository?)
if unknown_url? if unknown_url?
# In this case, we only want to import issues, not a repository. # In this case, we only want to import issues, not a repository.
create_repository create_repository
...@@ -44,12 +50,6 @@ module Projects ...@@ -44,12 +50,6 @@ module Projects
end end
def import_repository def import_repository
raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url)
# We should return early for a GitHub import because the new GitHub
# importer fetch the project repositories for us.
return if project.github_import?
begin begin
if project.gitea_import? if project.gitea_import?
fetch_repository fetch_repository
...@@ -88,7 +88,7 @@ module Projects ...@@ -88,7 +88,7 @@ module Projects
end end
def importer_class def importer_class
Gitlab::ImportSources.importer(project.import_type) @importer_class ||= Gitlab::ImportSources.importer(project.import_type)
end end
def has_importer? def has_importer?
......
.blank-state .blank-state-row
= link_to new_project_path, class: "blank-state-link" do
.blank-state
.blank-state-icon .blank-state-icon
= custom_icon("add_new_user", size: 50) = custom_icon("add_new_project", size: 50)
.blank-state-body .blank-state-body
%h3.blank-state-title %h3.blank-state-title
Add user Create a project
%p.blank-state-text %p.blank-state-text
Add your team members and others to GitLab. Projects are where you store your code, access issues, wiki and other features of GitLab.
= link_to new_admin_user_path, class: "btn btn-new" do
New user
.blank-state - if current_user.can_create_group?
= link_to admin_root_path, class: "blank-state-link" do
.blank-state
.blank-state-icon .blank-state-icon
= custom_icon("configure_server", size: 50) = custom_icon("add_new_group", size: 50)
.blank-state-body .blank-state-body
%h3.blank-state-title %h3.blank-state-title
Configure GitLab Create a group
%p.blank-state-text %p.blank-state-text
Make adjustments to how your GitLab instance is set up. Groups are a great way to organize projects and people.
= link_to admin_root_path, class: "btn btn-new" do
Configure
- if current_user.can_create_group? = link_to new_admin_user_path, class: "blank-state-link" do
.blank-state .blank-state
.blank-state-icon .blank-state-icon
= custom_icon("add_new_group", size: 50) = custom_icon("add_new_user", size: 50)
.blank-state-body .blank-state-body
%h3.blank-state-title %h3.blank-state-title
Create a group Add people
%p.blank-state-text %p.blank-state-text
Groups are a great way to organize projects and people. Add your team members and others to GitLab.
= link_to new_group_path, class: "btn btn-new" do
New group = link_to admin_root_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("configure_server", size: 50)
.blank-state-body
%h3.blank-state-title
Configure GitLab
%p.blank-state-text
Make adjustments to how your GitLab instance is set up.
- public_project_count = ProjectsFinder.new(current_user: current_user).execute.count - public_project_count = ProjectsFinder.new(current_user: current_user).execute.count
- if current_user.can_create_group? .blank-state-row
- if current_user.can_create_project?
= link_to new_project_path, class: "blank-state-link" do
.blank-state .blank-state
.blank-state-icon .blank-state-icon
= custom_icon("add_new_group", size: 50) = custom_icon("add_new_project", size: 50)
.blank-state-body .blank-state-body
%h3.blank-state-title %h3.blank-state-title
Create a group for several dependent projects. Create a project
%p.blank-state-text %p.blank-state-text
Groups are the best way to manage projects and members. Projects are where you store your code, access issues, wiki and other features of GitLab.
= link_to new_group_path, class: "btn btn-new" do - else
New group .blank-state
.blank-state
.blank-state-icon .blank-state-icon
= custom_icon("add_new_project", size: 50) = custom_icon("add_new_project", size: 50)
.blank-state-body .blank-state-body
%h3.blank-state-title %h3.blank-state-title
Create a project Create a project
%p.blank-state-text %p.blank-state-text
- if current_user.can_create_project?
You don't have access to any projects right now.
You can create up to
%strong= number_with_delimiter(current_user.projects_limit)
= succeed "." do
= "project".pluralize(current_user.projects_limit)
- else
If you are added to a project, it will be displayed here. If you are added to a project, it will be displayed here.
- if current_user.can_create_project?
= link_to new_project_path, class: "btn btn-new" do
New project
- if public_project_count > 0 - if current_user.can_create_group?
= link_to new_group_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("add_new_group", size: 50)
.blank-state-body
%h3.blank-state-title
Create a group
%p.blank-state-text
Groups are the best way to manage projects and members.
- if public_project_count > 0
= link_to trending_explore_projects_path, class: "blank-state-link" do
.blank-state .blank-state
.blank-state-icon .blank-state-icon
= custom_icon("globe", size: 50) = custom_icon("globe", size: 50)
...@@ -44,5 +46,13 @@ ...@@ -44,5 +46,13 @@
public projects on this server. public projects on this server.
Public projects are an easy way to allow Public projects are an easy way to allow
everyone to have read-only access. everyone to have read-only access.
= link_to trending_explore_projects_path, class: "btn btn-new" do
Browse projects = link_to "https://docs.gitlab.com/", class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("lightbulb", size: 50)
.blank-state-body
%h3.blank-state-title
Learn more about GitLab
%p.blank-state-text
Take a look at the documentation to discover all of GitLab's capabilities.
.row.blank-state-parent-container .blank-state-parent-container
.section-container.section-welcome{ class: "#{ 'section-admin-welcome' if current_user.admin? }" } .section-container.section-welcome{ class: "#{ 'section-admin-welcome' if current_user.admin? }" }
.container.section-body .container.section-body
.blank-state.blank-state-welcome .row
.blank-state-welcome
%h2.blank-state-welcome-title %h2.blank-state-welcome-title
Welcome to GitLab Welcome to GitLab
%p.blank-state-text %p.blank-state-text
......
...@@ -71,7 +71,7 @@ ...@@ -71,7 +71,7 @@
class: 'js-raw-link-controller has-tooltip controllers-buttons' do class: 'js-raw-link-controller has-tooltip controllers-buttons' do
= icon('file-text-o') = icon('file-text-o')
- if can?(current_user, :update_build, @project) && @build.erasable? - if @build.erasable? && can?(current_user, :erase_build, @build)
= link_to erase_project_job_path(@project, @build), = link_to erase_project_job_path(@project, @build),
method: :post, method: :post,
data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' }, data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
......
...@@ -9,12 +9,6 @@ ...@@ -9,12 +9,6 @@
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'), "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
"new-pipeline-path" => new_project_pipeline_path(@project), "new-pipeline-path" => new_project_pipeline_path(@project),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s, "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
"all-path" => project_pipelines_path(@project),
"pending-path" => project_pipelines_path(@project, scope: :pending),
"running-path" => project_pipelines_path(@project, scope: :running),
"finished-path" => project_pipelines_path(@project, scope: :finished),
"branches-path" => project_pipelines_path(@project, scope: :branches),
"tags-path" => project_pipelines_path(@project, scope: :tags),
"has-ci" => @repository.gitlab_ci_yml, "has-ci" => @repository.gitlab_ci_yml,
"ci-lint-path" => ci_lint_path } } "ci-lint-path" => ci_lint_path } }
......
...@@ -4,42 +4,39 @@ ...@@ -4,42 +4,39 @@
.row.prepend-top-default.append-bottom-default.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring .row.prepend-top-default.append-bottom-default.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring
.col-lg-3 .col-lg-3
%h4.prepend-top-0 %h4.prepend-top-0
Metrics = s_('PrometheusService|Metrics')
%p %p
Metrics are automatically configured and monitored = s_('PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters.')
based on a library of metrics from popular exporters. = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus')
= link_to 'More information', help_page_path('user/project/integrations/prometheus')
.col-lg-9 .col-lg-9
.panel.panel-default.js-panel-monitored-metrics{ data: { "active-metrics" => "#{project_prometheus_active_metrics_path(@project, :json)}" } } .panel.panel-default.js-panel-monitored-metrics{ data: { "active-metrics" => "#{project_prometheus_active_metrics_path(@project, :json)}" } }
.panel-heading .panel-heading
%h3.panel-title %h3.panel-title
Monitored = s_('PrometheusService|Monitored')
%span.badge.js-monitored-count 0 %span.badge.js-monitored-count 0
.panel-body .panel-body
.loading-metrics.text-center.js-loading-metrics .loading-metrics.text-center.js-loading-metrics
= icon('spinner spin 3x', class: 'metrics-load-spinner') = icon('spinner spin 3x', class: 'metrics-load-spinner')
%p Finding and configuring metrics... %p
= s_('PrometheusService|Finding and configuring metrics...')
.empty-metrics.text-center.hidden.js-empty-metrics .empty-metrics.text-center.hidden.js-empty-metrics
= custom_icon('icon_empty_metrics') = custom_icon('icon_empty_metrics')
%p No metrics are being monitored. To start monitoring, deploy to an environment. %p
= link_to project_environments_path(@project), title: 'View environments', class: 'btn btn-success' do = s_('PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment.')
View environments = link_to s_('PrometheusService|View environments'), project_environments_path(@project), class: 'btn btn-success'
%ul.list-unstyled.metrics-list.hidden.js-metrics-list %ul.list-unstyled.metrics-list.hidden.js-metrics-list
.panel.panel-default.hidden.js-panel-missing-env-vars .panel.panel-default.hidden.js-panel-missing-env-vars
.panel-heading .panel-heading
%h3.panel-title %h3.panel-title
= icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel') = icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel')
Missing environment variable = s_('PrometheusService|Missing environment variable')
%span.badge.js-env-var-count 0 %span.badge.js-env-var-count 0
.panel-body.hidden .panel-body.hidden
.flash-container .flash-container
.flash-notice .flash-notice
.flash-text .flash-text
To set up automatic monitoring, add the environment variable = s_("PrometheusService|To set up automatic monitoring, add the environment variable %{variable} to exporter's queries." % { variable: "<code>$CI_ENVIRONMENT_SLUG</code>" }).html_safe
%code = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus', anchor: 'metrics-and-labels')
$CI_ENVIRONMENT_SLUG
to exporter&rsquo;s queries.
= link_to 'More information', help_page_path('user/project/integrations/prometheus', anchor: 'metrics-and-labels')
%ul.list-unstyled.metrics-list.js-missing-var-metrics-list %ul.list-unstyled.metrics-list.js-missing-var-metrics-list
%tr.tree-truncated-warning
%td{ colspan: '3' }
= icon('exclamation-triangle fw')
%span
Too many items to show. To preserve performance only
%strong #{number_with_delimiter(limit)} of #{number_with_delimiter(total)}
items are displayed.
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M30 24a4 4 0 0 0-4 4v22a4 4 0 0 0 4 4h18a4 4 0 0 0 4-4V28a4 4 0 0 0-4-4H30zm0-4h18a8 8 0 0 1 8 8v22a8 8 0 0 1-8 8H30a8 8 0 0 1-8-8V28a8 8 0 0 1 8-8z"/><path fill="#FC6D26" d="M33 30h8a2 2 0 1 1 0 4h-8a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4z"/></g></svg> <svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M30 24c-2.21 0-4 1.79-4 4v22c0 2.21 1.79 4 4 4h18c2.21 0 4-1.79 4-4V28c0-2.21-1.79-4-4-4H30zm0-4h18c4.418 0 8 3.582 8 8v22c0 4.418-3.582 8-8 8H30c-4.418 0-8-3.582-8-8V28c0-4.418 3.582-8 8-8z"/><path fill="#6B4FBB" d="M33 30h8c1.105 0 2 .895 2 2s-.895 2-2 2h-8c-1.105 0-2-.895-2-2s.895-2 2-2zm0 7h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2zm0 7h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#6B4FBB" d="M33 52h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2zm1 5h10c1.105 0 2 .895 2 2s-.895 2-2 2H34c-1.105 0-2-.895-2-2s.895-2 2-2z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M45.542 46.932l.346-2.36c.198-1.348.737-2.623 1.566-3.705 3.025-3.946 4.485-7.29 4.547-9.96C52.153 24.41 46.843 20 39 20c-7.777 0-13 4.374-13 11 0 2.4 1.462 5.73 4.573 9.846.815 1.08 1.343 2.345 1.536 3.683l.353 2.456 13.08-.054zm-17.038.624L28.15 45.1c-.097-.67-.36-1.303-.768-1.842C23.794 38.51 22 34.424 22 31c0-9.39 7.61-15 17-15s17.218 5.614 17 15c-.085 3.64-1.875 7.74-5.37 12.3-.416.54-.685 1.18-.784 1.853l-.346 2.36c-.288 1.958-1.963 3.41-3.942 3.42l-13.08.053c-1.994.008-3.69-1.455-3.974-3.43z"/><path fill="#6B4FBB" d="M41 38.732c-.598-.345-1-.992-1-1.732 0-1.105.895-2 2-2s2 .895 2 2c0 .74-.402 1.387-1 1.732V42c0 .552-.448 1-1 1s-1-.448-1-1v-3.268zm-6 0c-.598-.345-1-.992-1-1.732 0-1.105.895-2 2-2s2 .895 2 2c0 .74-.402 1.387-1 1.732V42c0 .552-.448 1-1 1s-1-.448-1-1v-3.268z"/></g></svg>
...@@ -104,7 +104,6 @@ ...@@ -104,7 +104,6 @@
class: 'btn btn-remove prepend-left-10' class: 'btn btn-remove prepend-left-10'
- else - else
= link_to member, = link_to member,
remote: true,
method: :delete, method: :delete,
data: { confirm: remove_member_message(member) }, data: { confirm: remove_member_message(member) },
class: 'btn btn-remove prepend-left-10', class: 'btn btn-remove prepend-left-10',
......
...@@ -2,6 +2,8 @@ class UpdateMergeRequestsWorker ...@@ -2,6 +2,8 @@ class UpdateMergeRequestsWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
LOG_TIME_THRESHOLD = 90 # seconds
def perform(project_id, user_id, oldrev, newrev, ref) def perform(project_id, user_id, oldrev, newrev, ref)
project = Project.find_by(id: project_id) project = Project.find_by(id: project_id)
return unless project return unless project
...@@ -9,6 +11,20 @@ class UpdateMergeRequestsWorker ...@@ -9,6 +11,20 @@ class UpdateMergeRequestsWorker
user = User.find_by(id: user_id) user = User.find_by(id: user_id)
return unless user return unless user
# TODO: remove this benchmarking when we have rich logging
time = Benchmark.measure do
MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref) MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref)
end end
args_log = [
"elapsed=#{time.real}",
"project_id=#{project_id}",
"user_id=#{user_id}",
"oldrev=#{oldrev}",
"newrev=#{newrev}",
"ref=#{ref}"
].join(',')
Rails.logger.info("UpdateMergeRequestsWorker#perform #{args_log}") if time.real > LOG_TIME_THRESHOLD
end
end end
---
title: Stop reloading the page when using pagination and tabs - use API calls - in
Pipelines table
merge_request:
author:
type: other
---
title: Add internationalization support for the prometheus integration
merge_request: 33338
author:
type: other
---
title: Reorganize welcome page for new users
merge_request:
author:
type: other
---
title: Fix gitlab:backup rake for hashed storage based repositories
merge_request: 15400
author:
type: fixed
---
title: Fix pipeline status transition for single manual job. This would also fix pipeline
duration becuse it is depending on status transition
merge_request: 15251
author:
type: fixed
---
title: Add total_time_spent to the `changes` hash in issuable Webhook payloads
merge_request: 15381
author:
type: changed
---
title: Remove extra margin from wordmark in header
merge_request:
author:
type: fixed
---
title: Don't use JS to delete memberships from projects and groups
merge_request: 15344
author:
type: fixed
---
title: Make sure a user can add projects to subgroups they have access to
merge_request: 15294
author:
type: fixed
---
title: Prevent error when authorizing an admin-created OAauth application without
a set owner
merge_request:
author:
type: fixed
---
title: Enable UnnecessaryMantissa in scss-lint
merge_request: 15255
author: Takuya Noguchi
type: other
---
title: Fix filter by my reaction is not working
merge_request: 15345
author: Hiroyuki Sato
type: fixed
---
title: Only owner or master can erase jobs
merge_request: 15216
author:
type: changed
---
title: Truncate tree to max 1,000 items and display notice to users
merge_request:
author:
type: performance
---
title: 'Update emojis. Add :gay_pride_flag: and :speech_left:. Remove extraneous comma
in :cartwheel_tone4:'
merge_request:
author:
type: changed
---
title: Add performance logging to UpdateMergeRequestsWorker.
merge_request: 15360
author:
type: performance
...@@ -459,9 +459,9 @@ ...@@ -459,9 +459,9 @@
:versions: [] :versions: []
:when: 2017-09-13 17:31:16.425819400 Z :when: 2017-09-13 17:31:16.425819400 Z
- - :approve - - :approve
- gitlab-svgs - "@gitlab-org/gitlab-svgs"
- :who: Tim Zallmann - :who: Tim Zallmann
:why: Our own library - https://gitlab.com/gitlab-org/gitlab-svgs :why: Our own library - GitLab License https://gitlab.com/gitlab-org/gitlab-svgs
:versions: [] :versions: []
:when: 2017-09-19 14:36:32.795496000 Z :when: 2017-09-19 14:36:32.795496000 Z
- - :license - - :license
......
...@@ -145,7 +145,7 @@ ...@@ -145,7 +145,7 @@
- container_memory_usage_bytes - container_memory_usage_bytes
weight: 1 weight: 1
queries: queries:
- query_range: '(sum(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"})) /1024/1024' - query_range: '(sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024'
label: Average label: Average
unit: MB unit: MB
- title: "CPU Utilization" - title: "CPU Utilization"
...@@ -154,8 +154,6 @@ ...@@ -154,8 +154,6 @@
- container_cpu_usage_seconds_total - container_cpu_usage_seconds_total
weight: 1 weight: 1
queries: queries:
- query_range: 'sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) * 100' - query_range: 'sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100'
label: CPU label: Average
unit: "%" unit: "%"
\ No newline at end of file
series:
- label: cpu
...@@ -22,6 +22,7 @@ comments: false ...@@ -22,6 +22,7 @@ comments: false
- [UX guide](ux_guide/index.md) for building GitLab with existing CSS styles and elements - [UX guide](ux_guide/index.md) for building GitLab with existing CSS styles and elements
- [Frontend guidelines](fe_guide/index.md) - [Frontend guidelines](fe_guide/index.md)
- [Emoji guide](fe_guide/emojis.md)
## Backend guides ## Backend guides
......
# Emojis
GitLab supports native unicode emojis and fallsback to image-based emojis selectively
when your platform does not support it.
# How to update Emojis
1. Update the `gemojione` gem
1. Update `fixtures/emojis/index.json` from [Gemojione](https://github.com/jonathanwiesel/gemojione/blob/master/config/index.json).
In the future, we could grab the file directly from the gem.
We should probably make a PR on the Gemojione project to get access to
all emojis after being parsed or just a raw path to the `json` file itself.
1. Ensure [`emoji-unicode-version`](https://www.npmjs.com/package/emoji-unicode-version)
is up to date with the latest version.
1. Run `bundle exec rake gemojione:aliases`
1. Run `bundle exec rake gemojione:digests`
1. Run `bundle exec rake gemojione:sprite`
1. Ensure new sprite sheets generated for 1x and 2x
- `app/assets/images/emoji.png`
- `app/assets/images/emoji@2x.png`
1. Ensure you see new individual images copied into `app/assets/images/emoji/`
1. Ensure you can see the new emojis and their aliases in the GFM Autocomplete
1. Ensure you can see the new emojis and their aliases in the award emoji menu
1. You might need to add new emoji unicode support checks and rules for platforms
that do not support a certain emoji and we need to fallback to an image.
See `app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js`
and `app/assets/javascripts/emoji/support/unicode_support_map.js`
...@@ -29,7 +29,7 @@ Please use the following function inside JS to render an icon : ...@@ -29,7 +29,7 @@ Please use the following function inside JS to render an icon :
All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency. All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency.
To upgrade to a new SVG Sprite version run `yarn upgrade https://gitlab.com/gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders.
# SVG Illustrations # SVG Illustrations
......
...@@ -4,11 +4,11 @@ GitLab CE is licensed under the terms of the MIT License. GitLab EE is licensed ...@@ -4,11 +4,11 @@ GitLab CE is licensed under the terms of the MIT License. GitLab EE is licensed
## Automated Testing ## Automated Testing
In order to comply with the terms the libraries we use are licensed under, we have to make sure to check new gems for compatible licenses whenever they're added. To automate this process, we use the [license_finder][license_finder] gem by Pivotal. It runs every time a new commit is pushed and verifies that all gems in the bundle use a license that doesn't conflict with the licensing of either GitLab Community Edition or GitLab Enterprise Edition. In order to comply with the terms the libraries we use are licensed under, we have to make sure to check new gems for compatible licenses whenever they're added. To automate this process, we use the [license_finder][license_finder] gem by Pivotal. It runs every time a new commit is pushed and verifies that all gems and node modules in the bundle use a license that doesn't conflict with the licensing of either GitLab Community Edition or GitLab Enterprise Edition.
There are some limitations with the automated testing, however. CSS and JavaScript libraries, as well as any Ruby libraries not included by way of Bundler, must be verified manually and independently. Take care whenever one such library is used, as automated tests won't catch problematic licenses from them. There are some limitations with the automated testing, however. CSS, JavaScript, or Ruby libraries which are not included by way of Bundler, NPM, or Yarn (for instance those manually copied into our source tree in the `vendor` directory), must be verified manually and independently. Take care whenever one such library is used, as automated tests won't catch problematic licenses from them.
Some gems may not include their license information in their `gemspec` file. These won't be detected by License Finder, and will have to be verified manually. Some gems may not include their license information in their `gemspec` file, and some node modules may not include their license information in their `package.json` file. These won't be detected by License Finder, and will have to be verified manually.
### License Finder commands ### License Finder commands
......
...@@ -336,6 +336,12 @@ Blocks of code that are EE-specific should be moved to partials as much as ...@@ -336,6 +336,12 @@ Blocks of code that are EE-specific should be moved to partials as much as
possible to avoid conflicts with big chunks of HAML code that that are not fun possible to avoid conflicts with big chunks of HAML code that that are not fun
to resolve when you add the indentation in the equation. to resolve when you add the indentation in the equation.
### Assets
#### gitlab-svgs
Conflicts in `app/assets/images/icons.json` or `app/assets/images/icons.svg` can be resolved simply by regenerating those assets with [`yarn run svg`](https://gitlab.com/gitlab-org/gitlab-svgs).
--- ---
[Return to Development documentation](README.md) [Return to Development documentation](README.md)
...@@ -122,6 +122,15 @@ they can be easily inspected. ...@@ -122,6 +122,15 @@ they can be easily inspected.
bundle exec rake services:doc bundle exec rake services:doc
``` ```
## Updating Emoji Aliases
To update the Emoji aliases file (used for Emoji autocomplete) you must run the
following:
```
bundle exec rake gemojione:aliases
```
## Updating Emoji Digests ## Updating Emoji Digests
To update the Emoji digests file (used for Emoji autocomplete) you must run the To update the Emoji digests file (used for Emoji autocomplete) you must run the
...@@ -131,6 +140,7 @@ following: ...@@ -131,6 +140,7 @@ following:
bundle exec rake gemojione:digests bundle exec rake gemojione:digests
``` ```
This will update the file `fixtures/emojis/digests.json` based on the currently This will update the file `fixtures/emojis/digests.json` based on the currently
available Emoji. available Emoji.
......
...@@ -321,7 +321,7 @@ Auto DevOps uses [Helm](https://helm.sh/) to deploy your application to Kubernet ...@@ -321,7 +321,7 @@ Auto DevOps uses [Helm](https://helm.sh/) to deploy your application to Kubernet
You can override the Helm chart used by bundling up a chart into your project You can override the Helm chart used by bundling up a chart into your project
repo or by specifying a project variable: repo or by specifying a project variable:
- **Bundled chart** - If your project has a `./charts` directory with a `Chart.yaml` - **Bundled chart** - If your project has a `./chart` directory with a `Chart.yaml`
file in it, Auto DevOps will detect the chart and use it instead of the [default file in it, Auto DevOps will detect the chart and use it instead of the [default
one](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app). one](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app).
This can be a great way to control exactly how your application is deployed. This can be a great way to control exactly how your application is deployed.
......
...@@ -197,6 +197,7 @@ instance and project. In addition, all admins can use the admin interface under ...@@ -197,6 +197,7 @@ instance and project. In addition, all admins can use the admin interface under
|---------------------------------------|-----------------|-------------|----------|--------| |---------------------------------------|-----------------|-------------|----------|--------|
| See commits and jobs | ✓ | ✓ | ✓ | ✓ | | See commits and jobs | ✓ | ✓ | ✓ | ✓ |
| Retry or cancel job | | ✓ | ✓ | ✓ | | Retry or cancel job | | ✓ | ✓ | ✓ |
| Erase job artifacts and trace | | ✓ [^7] | ✓ | ✓ |
| Remove project | | | ✓ | ✓ | | Remove project | | | ✓ | ✓ |
| Create project | | | ✓ | ✓ | | Create project | | | ✓ | ✓ |
| Change project configuration | | | ✓ | ✓ | | Change project configuration | | | ✓ | ✓ |
...@@ -261,5 +262,6 @@ only. ...@@ -261,5 +262,6 @@ only.
[^4]: Not allowed for Guest, Reporter, Developer, Master, or Owner [^4]: Not allowed for Guest, Reporter, Developer, Master, or Owner
[^5]: Only if user is not external one. [^5]: Only if user is not external one.
[^6]: Only if user is a member of the project. [^6]: Only if user is a member of the project.
[^7]: Only if the build was triggered by the user
[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994 [ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994
[new-mod]: project/new_ci_build_permissions_model.md [new-mod]: project/new_ci_build_permissions_model.md
...@@ -13,8 +13,8 @@ integration services must be enabled. ...@@ -13,8 +13,8 @@ integration services must be enabled.
| Name | Query | | Name | Query |
| ---- | ----- | | ---- | ----- |
| Average Memory Usage (MB) | (sum(container_memory_usage_bytes{container_name!="POD",%{environment_filter}}) / count(container_memory_usage_bytes{container_name!="POD",%{environment_filter}})) /1024/1024 | | Average Memory Usage (MB) | (sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024 |
| Average CPU Utilization (%) | sum(rate(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}[2m])) by (cpu) * 100 | | Average CPU Utilization (%) | sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100 |
## Configuring Prometheus to monitor for Kubernetes node metrics ## Configuring Prometheus to monitor for Kubernetes node metrics
......
...@@ -62,7 +62,7 @@ which is highly recommendable and much faster than hardcoding. ...@@ -62,7 +62,7 @@ which is highly recommendable and much faster than hardcoding.
If you set up a GitLab Pages project on GitLab.com, If you set up a GitLab Pages project on GitLab.com,
it will automatically be accessible under a it will automatically be accessible under a
[subdomain of `namespace.pages.io`](introduction.md#gitlab-pages-on-gitlab-com). [subdomain of `namespace.gitlab.io`](introduction.md#gitlab-pages-on-gitlab-com).
The `namespace` is defined by your username on GitLab.com, The `namespace` is defined by your username on GitLab.com,
or the group name you created this project under. or the group name you created this project under.
......
...@@ -62,7 +62,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps ...@@ -62,7 +62,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end end
step 'I should see additional file lines' do step 'I should see additional file lines' do
page.within @diff.parent do page.within @diff.query_scope do
expect(first('.new_line').text).not_to have_content "..." expect(first('.new_line').text).not_to have_content "..."
end end
end end
......
...@@ -339,6 +339,7 @@ ...@@ -339,6 +339,7 @@
"baguette_bread":"french_bread", "baguette_bread":"french_bread",
"anguished":"frowning", "anguished":"frowning",
"white_frowning_face":"frowning2", "white_frowning_face":"frowning2",
"rainbow_flag":"gay_pride_flag",
"goal_net":"goal", "goal_net":"goal",
"hammer_and_pick":"hammer_pick", "hammer_and_pick":"hammer_pick",
"raised_hand_with_fingers_splayed":"hand_splayed", "raised_hand_with_fingers_splayed":"hand_splayed",
...@@ -488,6 +489,7 @@ ...@@ -488,6 +489,7 @@
"slightly_smiling_face":"slight_smile", "slightly_smiling_face":"slight_smile",
"sneeze":"sneezing_face", "sneeze":"sneezing_face",
"speaking_head_in_silhouette":"speaking_head", "speaking_head_in_silhouette":"speaking_head",
"left_speech_bubble":"speech_left",
"sleuth_or_spy":"spy", "sleuth_or_spy":"spy",
"sleuth_or_spy_tone1":"spy_tone1", "sleuth_or_spy_tone1":"spy_tone1",
"sleuth_or_spy_tone2":"spy_tone2", "sleuth_or_spy_tone2":"spy_tone2",
......
...@@ -1478,7 +1478,7 @@ ...@@ -1478,7 +1478,7 @@
}, },
"cartwheel_tone4": { "cartwheel_tone4": {
"category": "activity", "category": "activity",
"moji": "🤸🏾,", "moji": "🤸🏾",
"description": "person doing cartwheel tone 4", "description": "person doing cartwheel tone 4",
"unicodeVersion": "9.0", "unicodeVersion": "9.0",
"digest": "8253afb672431c84e498014c30babb00b9284bec773009e79f7f06aa7108643e" "digest": "8253afb672431c84e498014c30babb00b9284bec773009e79f7f06aa7108643e"
...@@ -5375,6 +5375,13 @@ ...@@ -5375,6 +5375,13 @@
"unicodeVersion": "6.0", "unicodeVersion": "6.0",
"digest": "180e66f19d9285e02d0a5e859722c608206826e80323942b9938fc49d44973b1" "digest": "180e66f19d9285e02d0a5e859722c608206826e80323942b9938fc49d44973b1"
}, },
"gay_pride_flag": {
"category": "flags",
"moji": "🏳🌈",
"description": "gay_pride_flag",
"unicodeVersion": "6.0",
"digest": "924e668c559db61b7f4724a661223081c2fc60d55169f3fe1ad6156934d1d37f"
},
"gemini": { "gemini": {
"category": "symbols", "category": "symbols",
"moji": "♊", "moji": "♊",
...@@ -7578,7 +7585,7 @@ ...@@ -7578,7 +7585,7 @@
"moji": "🤶", "moji": "🤶",
"description": "mother christmas", "description": "mother christmas",
"unicodeVersion": "9.0", "unicodeVersion": "9.0",
"digest": "1f72f586ca75bd7ebb4150cdcc8199a930c32fa4b81510cb8d200f1b3ddd4076" "digest": "357d769371305a8584f46d6087a962d647b6af22fab363a44702f38ab7814091"
}, },
"mrs_claus_tone1": { "mrs_claus_tone1": {
"category": "people", "category": "people",
...@@ -10709,6 +10716,13 @@ ...@@ -10709,6 +10716,13 @@
"unicodeVersion": "6.0", "unicodeVersion": "6.0",
"digest": "817100d9979456e7d2f253ac22e13b7a2302dc1590566214915b003e403c53ca" "digest": "817100d9979456e7d2f253ac22e13b7a2302dc1590566214915b003e403c53ca"
}, },
"speech_left": {
"category": "symbols",
"moji": "🗨",
"description": "left speech bubble",
"unicodeVersion": "7.0",
"digest": "912797107d574f5665411498b6e349dbdec69846f085b6dc356548c4155e90b0"
},
"speedboat": { "speedboat": {
"category": "travel", "category": "travel",
"moji": "🚤", "moji": "🚤",
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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