Commit b3e4bc59 authored by Jacob Schatz's avatar Jacob Schatz

resolves conflicts with new buttons

parents 7e43fa67 1100eb0c
Please view this file on the master branch, on stable branches it's out of date.
v 8.3.0 (unreleased)
v 8.4.0 (unreleased)
- Implement new UI for group page
- Add project permissions to all project API endpoints (Stan Hu)
v 8.3.0
- Add CAS support (tduehr)
- Bump rack-attack to 4.3.1 for security fix (Stan Hu)
- API support for starred projects for authorized user (Zeger-Jan van de Weg)
- Add link to merge request on build detail page.
- Add open_issues_count to project API (Stan Hu)
- Expand character set of usernames created by Omniauth (Corey Hinshaw)
- Add button to automatically merge a merge request when the build succeeds (Zeger-Jan van de Weg)
- Provide better diagnostic message upon project creation errors (Stan Hu)
- Bump devise to 3.5.3 to fix reset token expiring after account creation (Stan Hu)
- Remove api credentials from link to build_page
- Deprecate GitLabCiService making it to always be inactive
- Bump gollum-lib to 4.1.0 (Stan Hu)
- Fix broken group avatar upload under "New group" (Stan Hu)
- Update project repositorize size and commit count during import:repos task (Stan Hu)
......@@ -19,8 +27,10 @@ v 8.3.0 (unreleased)
- Fix 500 error when update group member permission
- Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera)
- Recognize issue/MR/snippet/commit links as references
- Backport JIRA features from EE to CE
- Add ignore whitespace change option to commit view
- Fire update hook from GitLab
- Allow account unlock via email
- Style warning about mentioning many people in a comment
- Fix: sort milestones by due date once again (Greg Smethells)
- Migrate all CI::Services and CI::WebHooks to Services and WebHooks
......@@ -58,6 +68,7 @@ v 8.3.0 (unreleased)
- Do not show build status unless builds are enabled and `.gitlab-ci.yml` is present
- Persist runners registration token in database
- Fix online editor should not remove newlines at the end of the file
- Expose Git's version in the admin area
v 8.2.3
- Fix application settings cache not expiring after changes (Stan Hu)
......
......@@ -358,7 +358,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[core team]: https://about.gitlab.com/core-team/
[getting help page]: https://about.gitlab.com/getting-help/
[Codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq
[up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=up+for+grabs
[up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=up-for-grabs
[medium-up-for-grabs]: https://medium.com/@kentcdodds/first-timers-only-78281ea47455
[ce-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/issues
[ee-tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues
......
......@@ -23,6 +23,7 @@ gem 'devise-async', '~> 0.9.0'
gem 'doorkeeper', '~> 2.2.0'
gem 'omniauth', '~> 1.2.2'
gem 'omniauth-bitbucket', '~> 0.0.2'
gem 'omniauth-cas3', '~> 1.1.2'
gem 'omniauth-facebook', '~> 3.0.0'
gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.0'
......@@ -101,6 +102,9 @@ gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
gem 'rouge', '~> 1.10.1'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
gem 'nokogiri', '1.6.7.1'
# Diffs
gem 'diffy', '~> 3.0.3'
......@@ -186,7 +190,7 @@ gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.3'
gem "sass-rails", '~> 4.0.5'
gem "sass-rails", '~> 5.0.0'
gem "coffee-rails", '~> 4.1.0'
gem "uglifier", '~> 2.7.2'
gem 'turbolinks', '~> 2.5.0'
......@@ -198,9 +202,9 @@ gem 'font-awesome-rails', '~> 4.2'
gem 'gitlab_emoji', '~> 0.2.0'
gem 'gon', '~> 6.0.1'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 3.1.3'
gem 'jquery-rails', '~> 4.0.0'
gem 'jquery-scrollto-rails', '~> 1.4.3'
gem 'jquery-ui-rails', '~> 4.2.1'
gem 'jquery-ui-rails', '~> 5.0.0'
gem 'nprogress-rails', '~> 0.1.6.7'
gem 'raphael-rails', '~> 2.1.2'
gem 'request_store', '~> 1.2.0'
......
......@@ -372,15 +372,16 @@ GEM
inflecto (0.0.2)
ipaddress (0.8.0)
jquery-atwho-rails (1.3.2)
jquery-rails (3.1.4)
railties (>= 3.0, < 5.0)
jquery-rails (4.0.5)
rails-dom-testing (~> 1.0)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
jquery-scrollto-rails (1.4.3)
railties (> 3.1, < 5.0)
jquery-turbolinks (2.1.0)
railties (>= 3.1.0)
turbolinks
jquery-ui-rails (4.2.1)
jquery-ui-rails (5.0.5)
railties (>= 3.2.16)
json (1.8.3)
jwt (1.5.2)
......@@ -420,7 +421,7 @@ GEM
grape
newrelic_rpm
newrelic_rpm (3.9.4.245)
nokogiri (1.6.7)
nokogiri (1.6.7.1)
mini_portile2 (~> 2.0.0.rc2)
nprogress-rails (0.1.6.7)
oauth (0.4.7)
......@@ -439,6 +440,10 @@ GEM
multi_json (~> 1.7)
omniauth (~> 1.1)
omniauth-oauth (~> 1.0)
omniauth-cas3 (1.1.3)
addressable (~> 2.3)
nokogiri (~> 1.6.6)
omniauth (~> 1.2)
omniauth-facebook (3.0.0)
omniauth-oauth2 (~> 1.2)
omniauth-github (1.1.2)
......@@ -643,12 +648,13 @@ GEM
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
sass (3.2.19)
sass-rails (4.0.5)
sass (3.4.20)
sass-rails (5.0.4)
railties (>= 4.0.0, < 5.0)
sass (~> 3.2.2)
sprockets (~> 2.8, < 3.0)
sprockets-rails (~> 2.0)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3)
sawyer (0.6.0)
addressable (~> 2.3.5)
faraday (~> 0.8, < 0.10)
......@@ -874,10 +880,10 @@ DEPENDENCIES
html-pipeline (~> 1.11.0)
httparty (~> 0.13.3)
jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 3.1.3)
jquery-rails (~> 4.0.0)
jquery-scrollto-rails (~> 1.4.3)
jquery-turbolinks (~> 2.1.0)
jquery-ui-rails (~> 4.2.1)
jquery-ui-rails (~> 5.0.0)
kaminari (~> 0.16.3)
letter_opener (~> 1.1.2)
mail_room (~> 0.6.1)
......@@ -888,11 +894,13 @@ DEPENDENCIES
net-ssh (~> 3.0.1)
newrelic-grape
newrelic_rpm (~> 3.9.4.245)
nokogiri (= 1.6.7.1)
nprogress-rails (~> 0.1.6.7)
oauth2 (~> 1.0.0)
octokit (~> 3.7.0)
omniauth (~> 1.2.2)
omniauth-bitbucket (~> 0.0.2)
omniauth-cas3 (~> 1.1.2)
omniauth-facebook (~> 3.0.0)
omniauth-github (~> 1.1.1)
omniauth-gitlab (~> 1.0.0)
......@@ -928,7 +936,7 @@ DEPENDENCIES
rubocop (~> 0.35.0)
ruby-fogbugz (~> 0.2.1)
sanitize (~> 2.0)
sass-rails (~> 4.0.5)
sass-rails (~> 5.0.0)
sdoc (~> 0.3.20)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
......
8.3.0.pre
8.4.0.pre
......@@ -5,7 +5,7 @@
# the compiled file.
#
#= require jquery
#= require jquery.ui.all
#= require jquery-ui
#= require jquery_ujs
#= require jquery.cookie
#= require jquery.endless-scroll
......
class @AwardsHandler
constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) ->
$(".add-award").click (event)->
event.stopPropagation()
event.preventDefault()
$(".emoji-menu").show()
$("html").click ->
if !$(event.target).closest(".emoji-menu").length
if $(".emoji-menu").is(":visible")
$(".emoji-menu").hide()
addAward: (emoji) ->
emoji = @normilizeEmojiName(emoji)
@postEmoji emoji, =>
@addAwardToEmojiBar(emoji)
addAwardToEmojiBar: (emoji, custom_path = '') ->
$(".emoji-menu").hide()
addAwardToEmojiBar: (emoji) ->
emoji = @normilizeEmojiName(emoji)
if @exist(emoji)
if @isActive(emoji)
......@@ -17,7 +28,7 @@ class @AwardsHandler
counter.parent().addClass("active")
@addMeToAuthorList(emoji)
else
@createEmoji(emoji, custom_path)
@createEmoji(emoji)
exist: (emoji) ->
@findEmojiIcon(emoji).length > 0
......@@ -54,31 +65,29 @@ class @AwardsHandler
resetTooltip: (award) ->
award.tooltip("destroy")
# "destroy" call is asynchronous, this is why we need to set timeout.
# "destroy" call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout.
setTimeout (->
award.tooltip()
), 200
createEmoji: (emoji, custom_path) ->
createEmoji: (emoji) ->
emojiCssClass = @resolveNameToCssClass(emoji)
nodes = []
nodes.push("<div class='award active' title='me'>")
nodes.push("<div class='icon' data-emoji='" + emoji + "'>")
nodes.push(@getImage(emoji, custom_path))
nodes.push("<div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div>")
nodes.push("<div class='counter'>1</div>")
nodes.push("</div>")
nodes.push("<div class='counter'>1")
nodes.push("</div></div>")
$(".awards-controls").before(nodes.join("\n"))
emoji_node = $(nodes.join("\n")).insertBefore(".awards-controls").find(".emoji-icon").data("emoji", emoji)
$(".award").tooltip()
getImage: (emoji, custom_path) ->
if custom_path
$("<img>").attr({src: custom_path, width: 20, height: 20}).wrap("<div>").parent().html()
else
$("li[data-emoji='" + emoji + "']").html()
resolveNameToCssClass: (emoji) ->
unicodeName = $(".emoji-menu-content [data-emoji='?']".replace("?", emoji)).data("unicode-name")
"emoji-" + unicodeName
postEmoji: (emoji, callback) ->
$.post @post_emoji_url, { note: {
......@@ -90,7 +99,7 @@ class @AwardsHandler
callback.call()
findEmojiIcon: (emoji) ->
$(".icon[data-emoji='" + emoji + "']")
$(".award [data-emoji='" + emoji + "']")
scrollToAwards: ->
$('body, html').animate({
......
......@@ -127,7 +127,7 @@ class @Notes
@initTaskList()
if note.award
awards_handler.addAwardToEmojiBar(note.note, note.emoji_path)
awards_handler.addAwardToEmojiBar(note.note)
awards_handler.scrollToAwards()
###
......
......@@ -2,8 +2,8 @@
* This is a manifest file that'll automatically include all the stylesheets available in this directory
* and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
* the top of the compiled file, but it's generally better to create a new file per style scope.
*= require jquery.ui.datepicker
*= require jquery.ui.autocomplete
*= require jquery-ui/datepicker
*= require jquery-ui/autocomplete
*= require jquery.atwho
*= require select2
*= require_self
......
......@@ -76,7 +76,7 @@
.cover-block {
text-align: center;
background: #f7f8fa;
background: $background-color;
margin: -$gl-padding;
margin-bottom: 0;
padding: 44px $gl-padding;
......
@mixin btn-default {
@include border-radius(2px);
@include border-radius(3px);
border-width: 1px;
border-style: solid;
text-transform: uppercase;
font-size: 13px;
font-weight: 600;
font-size: 15px;
font-weight: 500;
line-height: 18px;
padding: 11px $gl-padding;
letter-spacing: .4px;
......@@ -18,7 +17,7 @@
@mixin btn-middle {
@include btn-default;
@include border-radius(2px);
@include border-radius(3px);
padding: 11px 24px;
}
......@@ -51,6 +50,10 @@
@include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #FFFFFF);
}
@mixin btn-blue-medium {
@include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #FFFFFF);
}
@mixin btn-orange {
@include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #FFFFFF);
}
......@@ -60,7 +63,7 @@
}
@mixin btn-gray {
@include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, #313236);
@include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-light, $gray-dark, $border-gray-dark, #313236);
}
@mixin btn-white {
......@@ -75,6 +78,10 @@
padding: 5px 10px;
}
&.btn-nr {
padding: 7px 10px;
}
&.btn-xs {
padding: 1px 5px;
}
......@@ -91,11 +98,15 @@
@include btn-gray;
}
&.btn-primary,
&.btn-primary {
@include btn-blue-medium;
}
&.btn-info {
@include btn-blue;
}
&.btn-close,
&.btn-warning {
@include btn-orange;
}
......@@ -110,20 +121,8 @@
float: right;
}
&.btn-close {
color: $gl-danger;
border-color: $gl-danger;
&:hover {
color: #B94A48;
}
}
&.btn-reopen {
color: $gl-success;
border-color: $gl-success;
&:hover {
color: #468847;
}
/* should be same as parent class for now */
}
&.btn-grouped {
......
......@@ -374,7 +374,7 @@ table {
}
}
.center-top-menu {
.center-top-menu, .left-top-menu {
@include nav-menu;
text-align: center;
margin-top: 5px;
......@@ -408,6 +408,11 @@ table {
}
}
.left-top-menu {
text-align: left;
border-bottom: 1px solid #EEE;
}
.center-middle-menu {
@include nav-menu;
padding: 0;
......
......@@ -5,7 +5,7 @@
*/
.status-box {
@include border-radius(2px);
@include border-radius(3px);
display: block;
float: left;
......@@ -25,7 +25,7 @@
}
&.status-box-open {
background-color: #019875;
background-color: $green-light;
color: #FFF;
}
......
......@@ -5,7 +5,7 @@ html {
}
body {
background-color: #EAEBEC !important;
background-color: #F3F3F3 !important;
&.navless {
background-color: white !important;
......
......@@ -123,7 +123,6 @@
padding: 0;
margin: 0;
list-style: none;
margin-top: 5px;
height: 56px;
li {
......@@ -131,9 +130,9 @@
a {
padding: 14px;
font-size: 17px;
font-size: 15px;
line-height: 28px;
color: #7f8fa4;
color: #959494;
border-bottom: 2px solid transparent;
&:hover, &:active, &:focus {
......@@ -143,8 +142,8 @@
}
&.active a {
color: #4c4e54;
border-bottom: 2px solid #1cacfc;
color: #616060;
border-bottom: 2px solid #4688f1;
}
.badge {
......
......@@ -81,7 +81,7 @@
display: none;
}
.center-top-menu {
.center-top-menu, .left-top-menu {
li a {
font-size: 14px;
padding: 19px 10px;
......
$hover: #FFFAF1;
$hover: #faf9f9;
$gl-text-color: #54565B;
$gl-text-green: #4A2;
$gl-text-red: #D12F19;
$gl-text-orange: #D90;
$gl-header-color: #4c4e54;
$gl-header-color: #323232;
$gl-link-color: #333c48;
$md-text-color: #444;
$md-link-color: #3084bb;
......@@ -15,13 +15,14 @@ $sidebar_width: 230px;
$avatar_radius: 50%;
$code_font_size: 13px;
$code_line_height: 1.5;
$border-color: #dce0e6;
$border-color: #efeff1;
$table-border-color: #eef0f2;
$background-color: #F7F8FA;
$background-color: #faf9f9;
$header-height: 58px;
$fixed-layout-width: 1280px;
$gl-gray: #7f8fa4;
$gl-gray: #5a5a5a;
$gl-padding: 16px;
$gl-padding-top:10px;
$gl-avatar-size: 46px;
/*
......@@ -29,12 +30,12 @@ $gl-avatar-size: 46px;
*/
$white-light: #FFFFFF;
$white-normal: #DCE0E5;
$white-dark: #E4E7ED;
$white-normal: #ededed;
$white-dark: #ededed;
$gray-light: #F0F2F5;
$gray-normal: #DCE0E5;
$gray-dark: #E4E7ED;
$gray-light: #f7f7f7;
$gray-normal: #ededed;
$gray-dark: #ededed;
$green-light: #31AF64;
$green-normal: #2FAA60;
......@@ -44,6 +45,10 @@ $blue-light: #2EA8E5;
$blue-normal: #2D9FD8;
$blue-dark: #2897CE;
$blue-medium-light: #3498CB;
$blue-medium: #2F8EBF;
$blue-medium-dark: #2D86B4;
$orange-light: #FC6443;
$orange-normal: #E75E40;
$orange-dark: #CE5237;
......@@ -52,11 +57,11 @@ $red-light: #F43263;
$red-normal: #E52C5A;
$red-dark: #D22852;
$border-white-light: #E3E7EC;
$border-white-light: #F1F2F4;
$border-white-normal: #D6DAE2;
$border-white-dark: #C6CACF;
$border-gray-light: #DCE0E5;
$border-gray-light: #d1d1d1;
$border-gray-normal: #D6DAE2;
$border-gray-dark: #C6CACF;
......@@ -76,6 +81,8 @@ $border-red-light: #E52C5A;
$border-red-normal: #D22852;
$border-red-dark: #CA264F;
/* header */
$light-grey-header: #faf9f9;
/*
* State colors:
......
......@@ -2,6 +2,12 @@
@include clearfix;
line-height: 34px;
.emoji-icon {
width: 20px;
height: 20px;
margin: 7px 0 0 5px;
}
.award {
@include border-radius(5px);
......@@ -40,6 +46,7 @@
}
.awards-controls {
position: relative;
margin-left: 10px;
float: left;
......@@ -55,32 +62,60 @@
}
}
.awards-menu {
.emoji-menu{
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
display: none;
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
font-size: 14px;
text-align: left;
list-style: none;
background-color: #fff;
-webkit-background-clip: padding-box;
background-clip: padding-box;
border: 1px solid #ccc;
border: 1px solid rgba(0,0,0,.15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175);
box-shadow: 0 6px 12px rgba(0,0,0,.175);
.emoji-menu-content {
padding: $gl-padding;
min-width: 214px;
width: 300px;
height: 300px;
overflow-y: scroll;
> li {
h5 {
clear: left;
}
ul {
list-style-type: none;
margin-left: -20px;
margin-bottom: 20px;
overflow: auto;
}
li {
cursor: pointer;
width: 30px;
height: 30px;
text-align: center;
float: left;
margin: 3px;
list-decorate: none;
@include border-radius(5px);
img {
margin-bottom: 2px;
}
&:hover {
background-color: #ccc;
}
}
}
}
.awards-menu{
li {
float: left;
margin: 3px;
}
}
}
......@@ -5,7 +5,7 @@
border-bottom: 1px solid $border-color;
color: #5c5d5e;
font-size: 16px;
line-height: 42px;
line-height: 34px;
.author {
color: #5c5d5e;
......
/*
File is generated by https://github.com/jakesgordon/sprite-factory and midified manualy
The source: gemojione gem.
*/
.emoji-icon{
background-image: url(emoji.png);
background-repeat: no-repeat;
}
.emoji-0023-20E3 { background-position: 0px 0px; }
.emoji-0030-20E3 { background-position: -20px 0px; }
.emoji-0031-20E3 { background-position: -40px 0px; }
.emoji-0032-20E3 { background-position: -60px 0px; }
.emoji-0033-20E3 { background-position: -80px 0px; }
.emoji-0034-20E3 { background-position: -100px 0px; }
.emoji-0035-20E3 { background-position: -120px 0px; }
.emoji-0036-20E3 { background-position: -140px 0px; }
.emoji-0037-20E3 { background-position: -160px 0px; }
.emoji-0038-20E3 { background-position: -180px 0px; }
.emoji-0039-20E3 { background-position: -200px 0px; }
.emoji-00A9 { background-position: -220px 0px; }
.emoji-00AE { background-position: -240px 0px; }
.emoji-1F004 { background-position: -260px 0px; }
.emoji-1F0CF { background-position: -280px 0px; }
.emoji-1F170 { background-position: -300px 0px; }
.emoji-1F171 { background-position: -320px 0px; }
.emoji-1F17E { background-position: -340px 0px; }
.emoji-1F17F { background-position: -360px 0px; }
.emoji-1F18E { background-position: -380px 0px; }
.emoji-1F191 { background-position: -400px 0px; }
.emoji-1F192 { background-position: -420px 0px; }
.emoji-1F193 { background-position: -440px 0px; }
.emoji-1F194 { background-position: -460px 0px; }
.emoji-1F195 { background-position: -480px 0px; }
.emoji-1F196 { background-position: -500px 0px; }
.emoji-1F197 { background-position: -520px 0px; }
.emoji-1F198 { background-position: -540px 0px; }
.emoji-1F199 { background-position: -560px 0px; }
.emoji-1F19A { background-position: -580px 0px; }
.emoji-1F1E6-1F1E8 { background-position: -600px 0px; }
.emoji-1F1E6-1F1E9 { background-position: -620px 0px; }
.emoji-1F1E6-1F1EA { background-position: -640px 0px; }
.emoji-1F1E6-1F1EB { background-position: -660px 0px; }
.emoji-1F1E6-1F1EC { background-position: -680px 0px; }
.emoji-1F1E6-1F1EE { background-position: -700px 0px; }
.emoji-1F1E6-1F1F1 { background-position: -720px 0px; }
.emoji-1F1E6-1F1F2 { background-position: -740px 0px; }
.emoji-1F1E6-1F1F4 { background-position: -760px 0px; }
.emoji-1F1E6-1F1F7 { background-position: -780px 0px; }
.emoji-1F1E6-1F1F9 { background-position: -800px 0px; }
.emoji-1F1E6-1F1FA { background-position: -820px 0px; }
.emoji-1F1E6-1F1FC { background-position: -840px 0px; }
.emoji-1F1E6-1F1FF { background-position: -860px 0px; }
.emoji-1F1E7-1F1E6 { background-position: -880px 0px; }
.emoji-1F1E7-1F1E7 { background-position: -900px 0px; }
.emoji-1F1E7-1F1E9 { background-position: -920px 0px; }
.emoji-1F1E7-1F1EA { background-position: -940px 0px; }
.emoji-1F1E7-1F1EB { background-position: -960px 0px; }
.emoji-1F1E7-1F1EC { background-position: -980px 0px; }
.emoji-1F1E7-1F1ED { background-position: -1000px 0px; }
.emoji-1F1E7-1F1EE { background-position: -1020px 0px; }
.emoji-1F1E7-1F1EF { background-position: -1040px 0px; }
.emoji-1F1E7-1F1F2 { background-position: -1060px 0px; }
.emoji-1F1E7-1F1F3 { background-position: -1080px 0px; }
.emoji-1F1E7-1F1F4 { background-position: -1100px 0px; }
.emoji-1F1E7-1F1F7 { background-position: -1120px 0px; }
.emoji-1F1E7-1F1F8 { background-position: -1140px 0px; }
.emoji-1F1E7-1F1F9 { background-position: -1160px 0px; }
.emoji-1F1E7-1F1FC { background-position: -1180px 0px; }
.emoji-1F1E7-1F1FE { background-position: -1200px 0px; }
.emoji-1F1E7-1F1FF { background-position: -1220px 0px; }
.emoji-1F1E8-1F1E6 { background-position: -1240px 0px; }
.emoji-1F1E8-1F1E9 { background-position: -1260px 0px; }
.emoji-1F1E8-1F1EB { background-position: -1280px 0px; }
.emoji-1F1E8-1F1EC { background-position: -1300px 0px; }
.emoji-1F1E8-1F1ED { background-position: -1320px 0px; }
.emoji-1F1E8-1F1EE { background-position: -1340px 0px; }
.emoji-1F1E8-1F1F1 { background-position: -1360px 0px; }
.emoji-1F1E8-1F1F2 { background-position: -1380px 0px; }
.emoji-1F1E8-1F1F3 { background-position: -1400px 0px; }
.emoji-1F1E8-1F1F4 { background-position: -1420px 0px; }
.emoji-1F1E8-1F1F7 { background-position: -1440px 0px; }
.emoji-1F1E8-1F1FA { background-position: -1460px 0px; }
.emoji-1F1E8-1F1FB { background-position: -1480px 0px; }
.emoji-1F1E8-1F1FE { background-position: -1500px 0px; }
.emoji-1F1E8-1F1FF { background-position: -1520px 0px; }
.emoji-1F1E9-1F1EA { background-position: -1540px 0px; }
.emoji-1F1E9-1F1EF { background-position: -1560px 0px; }
.emoji-1F1E9-1F1F0 { background-position: -1580px 0px; }
.emoji-1F1E9-1F1F2 { background-position: -1600px 0px; }
.emoji-1F1E9-1F1F4 { background-position: -1620px 0px; }
.emoji-1F1E9-1F1FF { background-position: -1640px 0px; }
.emoji-1F1EA-1F1E8 { background-position: -1660px 0px; }
.emoji-1F1EA-1F1EA { background-position: -1680px 0px; }
.emoji-1F1EA-1F1EC { background-position: -1700px 0px; }
.emoji-1F1EA-1F1ED { background-position: -1720px 0px; }
.emoji-1F1EA-1F1F7 { background-position: -1740px 0px; }
.emoji-1F1EA-1F1F8 { background-position: -1760px 0px; }
.emoji-1F1EA-1F1F9 { background-position: -1780px 0px; }
.emoji-1F1EB-1F1EE { background-position: -1800px 0px; }
.emoji-1F1EB-1F1EF { background-position: -1820px 0px; }
.emoji-1F1EB-1F1F0 { background-position: -1840px 0px; }
.emoji-1F1EB-1F1F2 { background-position: -1860px 0px; }
.emoji-1F1EB-1F1F4 { background-position: -1880px 0px; }
.emoji-1F1EB-1F1F7 { background-position: -1900px 0px; }
.emoji-1F1EC-1F1E6 { background-position: -1920px 0px; }
.emoji-1F1EC-1F1E7 { background-position: -1940px 0px; }
.emoji-1F1EC-1F1E9 { background-position: -1960px 0px; }
.emoji-1F1EC-1F1EA { background-position: -1980px 0px; }
.emoji-1F1EC-1F1ED { background-position: -2000px 0px; }
.emoji-1F1EC-1F1EE { background-position: -2020px 0px; }
.emoji-1F1EC-1F1F1 { background-position: -2040px 0px; }
.emoji-1F1EC-1F1F2 { background-position: -2060px 0px; }
.emoji-1F1EC-1F1F3 { background-position: -2080px 0px; }
.emoji-1F1EC-1F1F6 { background-position: -2100px 0px; }
.emoji-1F1EC-1F1F7 { background-position: -2120px 0px; }
.emoji-1F1EC-1F1F9 { background-position: -2140px 0px; }
.emoji-1F1EC-1F1FA { background-position: -2160px 0px; }
.emoji-1F1EC-1F1FC { background-position: -2180px 0px; }
.emoji-1F1EC-1F1FE { background-position: -2200px 0px; }
.emoji-1F1ED-1F1F0 { background-position: -2220px 0px; }
.emoji-1F1ED-1F1F3 { background-position: -2240px 0px; }
.emoji-1F1ED-1F1F7 { background-position: -2260px 0px; }
.emoji-1F1ED-1F1F9 { background-position: -2280px 0px; }
.emoji-1F1ED-1F1FA { background-position: -2300px 0px; }
.emoji-1F1EE-1F1E9 { background-position: -2320px 0px; }
.emoji-1F1EE-1F1EA { background-position: -2340px 0px; }
.emoji-1F1EE-1F1F1 { background-position: -2360px 0px; }
.emoji-1F1EE-1F1F3 { background-position: -2380px 0px; }
.emoji-1F1EE-1F1F6 { background-position: -2400px 0px; }
.emoji-1F1EE-1F1F7 { background-position: -2420px 0px; }
.emoji-1F1EE-1F1F8 { background-position: -2440px 0px; }
.emoji-1F1EE-1F1F9 { background-position: -2460px 0px; }
.emoji-1F1EF-1F1EA { background-position: -2480px 0px; }
.emoji-1F1EF-1F1F2 { background-position: -2500px 0px; }
.emoji-1F1EF-1F1F4 { background-position: -2520px 0px; }
.emoji-1F1EF-1F1F5 { background-position: -2540px 0px; }
.emoji-1F1F0-1F1EA { background-position: -2560px 0px; }
.emoji-1F1F0-1F1EC { background-position: -2580px 0px; }
.emoji-1F1F0-1F1ED { background-position: -2600px 0px; }
.emoji-1F1F0-1F1EE { background-position: -2620px 0px; }
.emoji-1F1F0-1F1F2 { background-position: -2640px 0px; }
.emoji-1F1F0-1F1F3 { background-position: -2660px 0px; }
.emoji-1F1F0-1F1F5 { background-position: -2680px 0px; }
.emoji-1F1F0-1F1F7 { background-position: -2700px 0px; }
.emoji-1F1F0-1F1FC { background-position: -2720px 0px; }
.emoji-1F1F0-1F1FE { background-position: -2740px 0px; }
.emoji-1F1F0-1F1FF { background-position: -2760px 0px; }
.emoji-1F1F1-1F1E6 { background-position: -2780px 0px; }
.emoji-1F1F1-1F1E7 { background-position: -2800px 0px; }
.emoji-1F1F1-1F1E8 { background-position: -2820px 0px; }
.emoji-1F1F1-1F1EE { background-position: -2840px 0px; }
.emoji-1F1F1-1F1F0 { background-position: -2860px 0px; }
.emoji-1F1F1-1F1F7 { background-position: -2880px 0px; }
.emoji-1F1F1-1F1F8 { background-position: -2900px 0px; }
.emoji-1F1F1-1F1F9 { background-position: -2920px 0px; }
.emoji-1F1F1-1F1FA { background-position: -2940px 0px; }
.emoji-1F1F1-1F1FB { background-position: -2960px 0px; }
.emoji-1F1F1-1F1FE { background-position: -2980px 0px; }
.emoji-1F1F2-1F1E6 { background-position: -3000px 0px; }
.emoji-1F1F2-1F1E8 { background-position: -3020px 0px; }
.emoji-1F1F2-1F1E9 { background-position: -3040px 0px; }
.emoji-1F1F2-1F1EA { background-position: -3060px 0px; }
.emoji-1F1F2-1F1EC { background-position: -3080px 0px; }
.emoji-1F1F2-1F1ED { background-position: -3100px 0px; }
.emoji-1F1F2-1F1F0 { background-position: -3120px 0px; }
.emoji-1F1F2-1F1F1 { background-position: -3140px 0px; }
.emoji-1F1F2-1F1F2 { background-position: -3160px 0px; }
.emoji-1F1F2-1F1F3 { background-position: -3180px 0px; }
.emoji-1F1F2-1F1F4 { background-position: -3200px 0px; }
.emoji-1F1F2-1F1F7 { background-position: -3220px 0px; }
.emoji-1F1F2-1F1F8 { background-position: -3240px 0px; }
.emoji-1F1F2-1F1F9 { background-position: -3260px 0px; }
.emoji-1F1F2-1F1FA { background-position: -3280px 0px; }
.emoji-1F1F2-1F1FB { background-position: -3300px 0px; }
.emoji-1F1F2-1F1FC { background-position: -3320px 0px; }
.emoji-1F1F2-1F1FD { background-position: -3340px 0px; }
.emoji-1F1F2-1F1FE { background-position: -3360px 0px; }
.emoji-1F1F2-1F1FF { background-position: -3380px 0px; }
.emoji-1F1F3-1F1E6 { background-position: -3400px 0px; }
.emoji-1F1F3-1F1E8 { background-position: -3420px 0px; }
.emoji-1F1F3-1F1EA { background-position: -3440px 0px; }
.emoji-1F1F3-1F1EC { background-position: -3460px 0px; }
.emoji-1F1F3-1F1EE { background-position: -3480px 0px; }
.emoji-1F1F3-1F1F1 { background-position: -3500px 0px; }
.emoji-1F1F3-1F1F4 { background-position: -3520px 0px; }
.emoji-1F1F3-1F1F5 { background-position: -3540px 0px; }
.emoji-1F1F3-1F1F7 { background-position: -3560px 0px; }
.emoji-1F1F3-1F1FA { background-position: -3580px 0px; }
.emoji-1F1F3-1F1FF { background-position: -3600px 0px; }
.emoji-1F1F4-1F1F2 { background-position: -3620px 0px; }
.emoji-1F1F5-1F1E6 { background-position: -3640px 0px; }
.emoji-1F1F5-1F1EA { background-position: -3660px 0px; }
.emoji-1F1F5-1F1EB { background-position: -3680px 0px; }
.emoji-1F1F5-1F1EC { background-position: -3700px 0px; }
.emoji-1F1F5-1F1ED { background-position: -3720px 0px; }
.emoji-1F1F5-1F1F0 { background-position: -3740px 0px; }
.emoji-1F1F5-1F1F1 { background-position: -3760px 0px; }
.emoji-1F1F5-1F1F7 { background-position: -3780px 0px; }
.emoji-1F1F5-1F1F8 { background-position: -3800px 0px; }
.emoji-1F1F5-1F1F9 { background-position: -3820px 0px; }
.emoji-1F1F5-1F1FC { background-position: -3840px 0px; }
.emoji-1F1F5-1F1FE { background-position: -3860px 0px; }
.emoji-1F1F6-1F1E6 { background-position: -3880px 0px; }
.emoji-1F1F7-1F1F4 { background-position: -3900px 0px; }
.emoji-1F1F7-1F1F8 { background-position: -3920px 0px; }
.emoji-1F1F7-1F1FA { background-position: -3940px 0px; }
.emoji-1F1F7-1F1FC { background-position: -3960px 0px; }
.emoji-1F1F8-1F1E6 { background-position: -3980px 0px; }
.emoji-1F1F8-1F1E7 { background-position: -4000px 0px; }
.emoji-1F1F8-1F1E8 { background-position: -4020px 0px; }
.emoji-1F1F8-1F1E9 { background-position: -4040px 0px; }
.emoji-1F1F8-1F1EA { background-position: -4060px 0px; }
.emoji-1F1F8-1F1EC { background-position: -4080px 0px; }
.emoji-1F1F8-1F1ED { background-position: -4100px 0px; }
.emoji-1F1F8-1F1EE { background-position: -4120px 0px; }
.emoji-1F1F8-1F1F0 { background-position: -4140px 0px; }
.emoji-1F1F8-1F1F1 { background-position: -4160px 0px; }
.emoji-1F1F8-1F1F2 { background-position: -4180px 0px; }
.emoji-1F1F8-1F1F3 { background-position: -4200px 0px; }
.emoji-1F1F8-1F1F4 { background-position: -4220px 0px; }
.emoji-1F1F8-1F1F7 { background-position: -4240px 0px; }
.emoji-1F1F8-1F1F9 { background-position: -4260px 0px; }
.emoji-1F1F8-1F1FB { background-position: -4280px 0px; }
.emoji-1F1F8-1F1FE { background-position: -4300px 0px; }
.emoji-1F1F8-1F1FF { background-position: -4320px 0px; }
.emoji-1F1F9-1F1E9 { background-position: -4340px 0px; }
.emoji-1F1F9-1F1EC { background-position: -4360px 0px; }
.emoji-1F1F9-1F1ED { background-position: -4380px 0px; }
.emoji-1F1F9-1F1EF { background-position: -4400px 0px; }
.emoji-1F1F9-1F1F1 { background-position: -4420px 0px; }
.emoji-1F1F9-1F1F2 { background-position: -4440px 0px; }
.emoji-1F1F9-1F1F3 { background-position: -4460px 0px; }
.emoji-1F1F9-1F1F4 { background-position: -4480px 0px; }
.emoji-1F1F9-1F1F7 { background-position: -4500px 0px; }
.emoji-1F1F9-1F1F9 { background-position: -4520px 0px; }
.emoji-1F1F9-1F1FB { background-position: -4540px 0px; }
.emoji-1F1F9-1F1FC { background-position: -4560px 0px; }
.emoji-1F1F9-1F1FF { background-position: -4580px 0px; }
.emoji-1F1FA-1F1E6 { background-position: -4600px 0px; }
.emoji-1F1FA-1F1EC { background-position: -4620px 0px; }
.emoji-1F1FA-1F1F8 { background-position: -4640px 0px; }
.emoji-1F1FA-1F1FE { background-position: -4660px 0px; }
.emoji-1F1FA-1F1FF { background-position: -4680px 0px; }
.emoji-1F1FB-1F1E6 { background-position: -4700px 0px; }
.emoji-1F1FB-1F1E8 { background-position: -4720px 0px; }
.emoji-1F1FB-1F1EA { background-position: -4740px 0px; }
.emoji-1F1FB-1F1EE { background-position: -4760px 0px; }
.emoji-1F1FB-1F1F3 { background-position: -4780px 0px; }
.emoji-1F1FB-1F1FA { background-position: -4800px 0px; }
.emoji-1F1FC-1F1EB { background-position: -4820px 0px; }
.emoji-1F1FC-1F1F8 { background-position: -4840px 0px; }
.emoji-1F1FD-1F1F0 { background-position: -4860px 0px; }
.emoji-1F1FE-1F1EA { background-position: -4880px 0px; }
.emoji-1F1FF-1F1E6 { background-position: -4900px 0px; }
.emoji-1F1FF-1F1F2 { background-position: -4920px 0px; }
.emoji-1F1FF-1F1FC { background-position: -4940px 0px; }
.emoji-1F201 { background-position: -4960px 0px; }
.emoji-1F202 { background-position: -4980px 0px; }
.emoji-1F21A { background-position: -5000px 0px; }
.emoji-1F22F { background-position: -5020px 0px; }
.emoji-1F232 { background-position: -5040px 0px; }
.emoji-1F233 { background-position: -5060px 0px; }
.emoji-1F234 { background-position: -5080px 0px; }
.emoji-1F235 { background-position: -5100px 0px; }
.emoji-1F236 { background-position: -5120px 0px; }
.emoji-1F237 { background-position: -5140px 0px; }
.emoji-1F238 { background-position: -5160px 0px; }
.emoji-1F239 { background-position: -5180px 0px; }
.emoji-1F23A { background-position: -5200px 0px; }
.emoji-1F250 { background-position: -5220px 0px; }
.emoji-1F251 { background-position: -5240px 0px; }
.emoji-1F300 { background-position: -5260px 0px; }
.emoji-1F301 { background-position: -5280px 0px; }
.emoji-1F302 { background-position: -5300px 0px; }
.emoji-1F303 { background-position: -5320px 0px; }
.emoji-1F304 { background-position: -5340px 0px; }
.emoji-1F305 { background-position: -5360px 0px; }
.emoji-1F306 { background-position: -5380px 0px; }
.emoji-1F307 { background-position: -5400px 0px; }
.emoji-1F308 { background-position: -5420px 0px; }
.emoji-1F309 { background-position: -5440px 0px; }
.emoji-1F30A { background-position: -5460px 0px; }
.emoji-1F30B { background-position: -5480px 0px; }
.emoji-1F30C { background-position: -5500px 0px; }
.emoji-1F30D { background-position: -5520px 0px; }
.emoji-1F30E { background-position: -5540px 0px; }
.emoji-1F30F { background-position: -5560px 0px; }
.emoji-1F310 { background-position: -5580px 0px; }
.emoji-1F311 { background-position: -5600px 0px; }
.emoji-1F312 { background-position: -5620px 0px; }
.emoji-1F313 { background-position: -5640px 0px; }
.emoji-1F314 { background-position: -5660px 0px; }
.emoji-1F315 { background-position: -5680px 0px; }
.emoji-1F316 { background-position: -5700px 0px; }
.emoji-1F317 { background-position: -5720px 0px; }
.emoji-1F318 { background-position: -5740px 0px; }
.emoji-1F319 { background-position: -5760px 0px; }
.emoji-1F31A { background-position: -5780px 0px; }
.emoji-1F31B { background-position: -5800px 0px; }
.emoji-1F31C { background-position: -5820px 0px; }
.emoji-1F31D { background-position: -5840px 0px; }
.emoji-1F31E { background-position: -5860px 0px; }
.emoji-1F31F { background-position: -5880px 0px; }
.emoji-1F320 { background-position: -5900px 0px; }
.emoji-1F321 { background-position: -5920px 0px; }
.emoji-1F327 { background-position: -5940px 0px; }
.emoji-1F328 { background-position: -5960px 0px; }
.emoji-1F329 { background-position: -5980px 0px; }
.emoji-1F32A { background-position: -6000px 0px; }
.emoji-1F32B { background-position: -6020px 0px; }
.emoji-1F32C { background-position: -6040px 0px; }
.emoji-1F330 { background-position: -6060px 0px; }
.emoji-1F331 { background-position: -6080px 0px; }
.emoji-1F332 { background-position: -6100px 0px; }
.emoji-1F333 { background-position: -6120px 0px; }
.emoji-1F334 { background-position: -6140px 0px; }
.emoji-1F335 { background-position: -6160px 0px; }
.emoji-1F336 { background-position: -6180px 0px; }
.emoji-1F337 { background-position: -6200px 0px; }
.emoji-1F338 { background-position: -6220px 0px; }
.emoji-1F339 { background-position: -6240px 0px; }
.emoji-1F33A { background-position: -6260px 0px; }
.emoji-1F33B { background-position: -6280px 0px; }
.emoji-1F33C { background-position: -6300px 0px; }
.emoji-1F33D { background-position: -6320px 0px; }
.emoji-1F33E { background-position: -6340px 0px; }
.emoji-1F33F { background-position: -6360px 0px; }
.emoji-1F340 { background-position: -6380px 0px; }
.emoji-1F341 { background-position: -6400px 0px; }
.emoji-1F342 { background-position: -6420px 0px; }
.emoji-1F343 { background-position: -6440px 0px; }
.emoji-1F344 { background-position: -6460px 0px; }
.emoji-1F345 { background-position: -6480px 0px; }
.emoji-1F346 { background-position: -6500px 0px; }
.emoji-1F347 { background-position: -6520px 0px; }
.emoji-1F348 { background-position: -6540px 0px; }
.emoji-1F349 { background-position: -6560px 0px; }
.emoji-1F34A { background-position: -6580px 0px; }
.emoji-1F34B { background-position: -6600px 0px; }
.emoji-1F34C { background-position: -6620px 0px; }
.emoji-1F34D { background-position: -6640px 0px; }
.emoji-1F34E { background-position: -6660px 0px; }
.emoji-1F34F { background-position: -6680px 0px; }
.emoji-1F350 { background-position: -6700px 0px; }
.emoji-1F351 { background-position: -6720px 0px; }
.emoji-1F352 { background-position: -6740px 0px; }
.emoji-1F353 { background-position: -6760px 0px; }
.emoji-1F354 { background-position: -6780px 0px; }
.emoji-1F355 { background-position: -6800px 0px; }
.emoji-1F356 { background-position: -6820px 0px; }
.emoji-1F357 { background-position: -6840px 0px; }
.emoji-1F358 { background-position: -6860px 0px; }
.emoji-1F359 { background-position: -6880px 0px; }
.emoji-1F35A { background-position: -6900px 0px; }
.emoji-1F35B { background-position: -6920px 0px; }
.emoji-1F35C { background-position: -6940px 0px; }
.emoji-1F35D { background-position: -6960px 0px; }
.emoji-1F35E { background-position: -6980px 0px; }
.emoji-1F35F { background-position: -7000px 0px; }
.emoji-1F360 { background-position: -7020px 0px; }
.emoji-1F361 { background-position: -7040px 0px; }
.emoji-1F362 { background-position: -7060px 0px; }
.emoji-1F363 { background-position: -7080px 0px; }
.emoji-1F364 { background-position: -7100px 0px; }
.emoji-1F365 { background-position: -7120px 0px; }
.emoji-1F366 { background-position: -7140px 0px; }
.emoji-1F367 { background-position: -7160px 0px; }
.emoji-1F368 { background-position: -7180px 0px; }
.emoji-1F369 { background-position: -7200px 0px; }
.emoji-1F36A { background-position: -7220px 0px; }
.emoji-1F36B { background-position: -7240px 0px; }
.emoji-1F36C { background-position: -7260px 0px; }
.emoji-1F36D { background-position: -7280px 0px; }
.emoji-1F36E { background-position: -7300px 0px; }
.emoji-1F36F { background-position: -7320px 0px; }
.emoji-1F370 { background-position: -7340px 0px; }
.emoji-1F371 { background-position: -7360px 0px; }
.emoji-1F372 { background-position: -7380px 0px; }
.emoji-1F373 { background-position: -7400px 0px; }
.emoji-1F374 { background-position: -7420px 0px; }
.emoji-1F375 { background-position: -7440px 0px; }
.emoji-1F376 { background-position: -7460px 0px; }
.emoji-1F377 { background-position: -7480px 0px; }
.emoji-1F378 { background-position: -7500px 0px; }
.emoji-1F379 { background-position: -7520px 0px; }
.emoji-1F37A { background-position: -7540px 0px; }
.emoji-1F37B { background-position: -7560px 0px; }
.emoji-1F37C { background-position: -7580px 0px; }
.emoji-1F37D { background-position: -7600px 0px; }
.emoji-1F380 { background-position: -7620px 0px; }
.emoji-1F381 { background-position: -7640px 0px; }
.emoji-1F382 { background-position: -7660px 0px; }
.emoji-1F383 { background-position: -7680px 0px; }
.emoji-1F384 { background-position: -7700px 0px; }
.emoji-1F385 { background-position: -7720px 0px; }
.emoji-1F386 { background-position: -7740px 0px; }
.emoji-1F387 { background-position: -7760px 0px; }
.emoji-1F388 { background-position: -7780px 0px; }
.emoji-1F389 { background-position: -7800px 0px; }
.emoji-1F38A { background-position: -7820px 0px; }
.emoji-1F38B { background-position: -7840px 0px; }
.emoji-1F38C { background-position: -7860px 0px; }
.emoji-1F38D { background-position: -7880px 0px; }
.emoji-1F38E { background-position: -7900px 0px; }
.emoji-1F38F { background-position: -7920px 0px; }
.emoji-1F390 { background-position: -7940px 0px; }
.emoji-1F391 { background-position: -7960px 0px; }
.emoji-1F392 { background-position: -7980px 0px; }
.emoji-1F393 { background-position: -8000px 0px; }
.emoji-1F394 { background-position: -8020px 0px; }
.emoji-1F395 { background-position: -8040px 0px; }
.emoji-1F396 { background-position: -8060px 0px; }
.emoji-1F397 { background-position: -8080px 0px; }
.emoji-1F398 { background-position: -8100px 0px; }
.emoji-1F399 { background-position: -8120px 0px; }
.emoji-1F39A { background-position: -8140px 0px; }
.emoji-1F39B { background-position: -8160px 0px; }
.emoji-1F39C { background-position: -8180px 0px; }
.emoji-1F39D { background-position: -8200px 0px; }
.emoji-1F39E { background-position: -8220px 0px; }
.emoji-1F39F { background-position: -8240px 0px; }
.emoji-1F3A0 { background-position: -8260px 0px; }
.emoji-1F3A1 { background-position: -8280px 0px; }
.emoji-1F3A2 { background-position: -8300px 0px; }
.emoji-1F3A3 { background-position: -8320px 0px; }
.emoji-1F3A4 { background-position: -8340px 0px; }
.emoji-1F3A5 { background-position: -8360px 0px; }
.emoji-1F3A6 { background-position: -8380px 0px; }
.emoji-1F3A7 { background-position: -8400px 0px; }
.emoji-1F3A8 { background-position: -8420px 0px; }
.emoji-1F3A9 { background-position: -8440px 0px; }
.emoji-1F3AA { background-position: -8460px 0px; }
.emoji-1F3AB { background-position: -8480px 0px; }
.emoji-1F3AC { background-position: -8500px 0px; }
.emoji-1F3AD { background-position: -8520px 0px; }
.emoji-1F3AE { background-position: -8540px 0px; }
.emoji-1F3AF { background-position: -8560px 0px; }
.emoji-1F3B0 { background-position: -8580px 0px; }
.emoji-1F3B1 { background-position: -8600px 0px; }
.emoji-1F3B2 { background-position: -8620px 0px; }
.emoji-1F3B3 { background-position: -8640px 0px; }
.emoji-1F3B4 { background-position: -8660px 0px; }
.emoji-1F3B5 { background-position: -8680px 0px; }
.emoji-1F3B6 { background-position: -8700px 0px; }
.emoji-1F3B7 { background-position: -8720px 0px; }
.emoji-1F3B8 { background-position: -8740px 0px; }
.emoji-1F3B9 { background-position: -8760px 0px; }
.emoji-1F3BA { background-position: -8780px 0px; }
.emoji-1F3BB { background-position: -8800px 0px; }
.emoji-1F3BC { background-position: -8820px 0px; }
.emoji-1F3BD { background-position: -8840px 0px; }
.emoji-1F3BE { background-position: -8860px 0px; }
.emoji-1F3BF { background-position: -8880px 0px; }
.emoji-1F3C0 { background-position: -8900px 0px; }
.emoji-1F3C1 { background-position: -8920px 0px; }
.emoji-1F3C2 { background-position: -8940px 0px; }
.emoji-1F3C3 { background-position: -8960px 0px; }
.emoji-1F3C4 { background-position: -8980px 0px; }
.emoji-1F3C5 { background-position: -9000px 0px; }
.emoji-1F3C6 { background-position: -9020px 0px; }
.emoji-1F3C7 { background-position: -9040px 0px; }
.emoji-1F3C8 { background-position: -9060px 0px; }
.emoji-1F3C9 { background-position: -9080px 0px; }
.emoji-1F3CA { background-position: -9100px 0px; }
.emoji-1F3CB { background-position: -9120px 0px; }
.emoji-1F3CC { background-position: -9140px 0px; }
.emoji-1F3CD { background-position: -9160px 0px; }
.emoji-1F3CE { background-position: -9180px 0px; }
.emoji-1F3D4 { background-position: -9200px 0px; }
.emoji-1F3D5 { background-position: -9220px 0px; }
.emoji-1F3D6 { background-position: -9240px 0px; }
.emoji-1F3D7 { background-position: -9260px 0px; }
.emoji-1F3D8 { background-position: -9280px 0px; }
.emoji-1F3D9 { background-position: -9300px 0px; }
.emoji-1F3DA { background-position: -9320px 0px; }
.emoji-1F3DB { background-position: -9340px 0px; }
.emoji-1F3DC { background-position: -9360px 0px; }
.emoji-1F3DD { background-position: -9380px 0px; }
.emoji-1F3DE { background-position: -9400px 0px; }
.emoji-1F3DF { background-position: -9420px 0px; }
.emoji-1F3E0 { background-position: -9440px 0px; }
.emoji-1F3E1 { background-position: -9460px 0px; }
.emoji-1F3E2 { background-position: -9480px 0px; }
.emoji-1F3E3 { background-position: -9500px 0px; }
.emoji-1F3E4 { background-position: -9520px 0px; }
.emoji-1F3E5 { background-position: -9540px 0px; }
.emoji-1F3E6 { background-position: -9560px 0px; }
.emoji-1F3E7 { background-position: -9580px 0px; }
.emoji-1F3E8 { background-position: -9600px 0px; }
.emoji-1F3E9 { background-position: -9620px 0px; }
.emoji-1F3EA { background-position: -9640px 0px; }
.emoji-1F3EB { background-position: -9660px 0px; }
.emoji-1F3EC { background-position: -9680px 0px; }
.emoji-1F3ED { background-position: -9700px 0px; }
.emoji-1F3EE { background-position: -9720px 0px; }
.emoji-1F3EF { background-position: -9740px 0px; }
.emoji-1F3F0 { background-position: -9760px 0px; }
.emoji-1F3F1 { background-position: -9780px 0px; }
.emoji-1F3F2 { background-position: -9800px 0px; }
.emoji-1F3F3 { background-position: -9820px 0px; }
.emoji-1F3F4 { background-position: -9840px 0px; }
.emoji-1F3F5 { background-position: -9860px 0px; }
.emoji-1F3F6 { background-position: -9880px 0px; }
.emoji-1F3F7 { background-position: -9900px 0px; }
.emoji-1F400 { background-position: -9920px 0px; }
.emoji-1F401 { background-position: -9940px 0px; }
.emoji-1F402 { background-position: -9960px 0px; }
.emoji-1F403 { background-position: -9980px 0px; }
.emoji-1F404 { background-position: -10000px 0px; }
.emoji-1F405 { background-position: -10020px 0px; }
.emoji-1F406 { background-position: -10040px 0px; }
.emoji-1F407 { background-position: -10060px 0px; }
.emoji-1F408 { background-position: -10080px 0px; }
.emoji-1F409 { background-position: -10100px 0px; }
.emoji-1F40A { background-position: -10120px 0px; }
.emoji-1F40B { background-position: -10140px 0px; }
.emoji-1F40C { background-position: -10160px 0px; }
.emoji-1F40D { background-position: -10180px 0px; }
.emoji-1F40E { background-position: -10200px 0px; }
.emoji-1F40F { background-position: -10220px 0px; }
.emoji-1F410 { background-position: -10240px 0px; }
.emoji-1F411 { background-position: -10260px 0px; }
.emoji-1F412 { background-position: -10280px 0px; }
.emoji-1F413 { background-position: -10300px 0px; }
.emoji-1F414 { background-position: -10320px 0px; }
.emoji-1F415 { background-position: -10340px 0px; }
.emoji-1F416 { background-position: -10360px 0px; }
.emoji-1F417 { background-position: -10380px 0px; }
.emoji-1F418 { background-position: -10400px 0px; }
.emoji-1F419 { background-position: -10420px 0px; }
.emoji-1F41A { background-position: -10440px 0px; }
.emoji-1F41B { background-position: -10460px 0px; }
.emoji-1F41C { background-position: -10480px 0px; }
.emoji-1F41D { background-position: -10500px 0px; }
.emoji-1F41E { background-position: -10520px 0px; }
.emoji-1F41F { background-position: -10540px 0px; }
.emoji-1F420 { background-position: -10560px 0px; }
.emoji-1F421 { background-position: -10580px 0px; }
.emoji-1F422 { background-position: -10600px 0px; }
.emoji-1F423 { background-position: -10620px 0px; }
.emoji-1F424 { background-position: -10640px 0px; }
.emoji-1F425 { background-position: -10660px 0px; }
.emoji-1F426 { background-position: -10680px 0px; }
.emoji-1F427 { background-position: -10700px 0px; }
.emoji-1F428 { background-position: -10720px 0px; }
.emoji-1F429 { background-position: -10740px 0px; }
.emoji-1F42A { background-position: -10760px 0px; }
.emoji-1F42B { background-position: -10780px 0px; }
.emoji-1F42C { background-position: -10800px 0px; }
.emoji-1F42D { background-position: -10820px 0px; }
.emoji-1F42E { background-position: -10840px 0px; }
.emoji-1F42F { background-position: -10860px 0px; }
.emoji-1F430 { background-position: -10880px 0px; }
.emoji-1F431 { background-position: -10900px 0px; }
.emoji-1F432 { background-position: -10920px 0px; }
.emoji-1F433 { background-position: -10940px 0px; }
.emoji-1F434 { background-position: -10960px 0px; }
.emoji-1F435 { background-position: -10980px 0px; }
.emoji-1F436 { background-position: -11000px 0px; }
.emoji-1F437 { background-position: -11020px 0px; }
.emoji-1F438 { background-position: -11040px 0px; }
.emoji-1F439 { background-position: -11060px 0px; }
.emoji-1F43A { background-position: -11080px 0px; }
.emoji-1F43B { background-position: -11100px 0px; }
.emoji-1F43C { background-position: -11120px 0px; }
.emoji-1F43D { background-position: -11140px 0px; }
.emoji-1F43E { background-position: -11160px 0px; }
.emoji-1F43F { background-position: -11180px 0px; }
.emoji-1F440 { background-position: -11200px 0px; }
.emoji-1F441 { background-position: -11220px 0px; }
.emoji-1F442 { background-position: -11240px 0px; }
.emoji-1F443 { background-position: -11260px 0px; }
.emoji-1F444 { background-position: -11280px 0px; }
.emoji-1F445 { background-position: -11300px 0px; }
.emoji-1F446 { background-position: -11320px 0px; }
.emoji-1F447 { background-position: -11340px 0px; }
.emoji-1F448 { background-position: -11360px 0px; }
.emoji-1F449 { background-position: -11380px 0px; }
.emoji-1F44A { background-position: -11400px 0px; }
.emoji-1F44B { background-position: -11420px 0px; }
.emoji-1F44C { background-position: -11440px 0px; }
.emoji-1F44D { background-position: -11460px 0px; }
.emoji-1F44E { background-position: -11480px 0px; }
.emoji-1F44F { background-position: -11500px 0px; }
.emoji-1F450 { background-position: -11520px 0px; }
.emoji-1F451 { background-position: -11540px 0px; }
.emoji-1F452 { background-position: -11560px 0px; }
.emoji-1F453 { background-position: -11580px 0px; }
.emoji-1F454 { background-position: -11600px 0px; }
.emoji-1F455 { background-position: -11620px 0px; }
.emoji-1F456 { background-position: -11640px 0px; }
.emoji-1F457 { background-position: -11660px 0px; }
.emoji-1F458 { background-position: -11680px 0px; }
.emoji-1F459 { background-position: -11700px 0px; }
.emoji-1F45A { background-position: -11720px 0px; }
.emoji-1F45B { background-position: -11740px 0px; }
.emoji-1F45C { background-position: -11760px 0px; }
.emoji-1F45D { background-position: -11780px 0px; }
.emoji-1F45E { background-position: -11800px 0px; }
.emoji-1F45F { background-position: -11820px 0px; }
.emoji-1F460 { background-position: -11840px 0px; }
.emoji-1F461 { background-position: -11860px 0px; }
.emoji-1F462 { background-position: -11880px 0px; }
.emoji-1F463 { background-position: -11900px 0px; }
.emoji-1F464 { background-position: -11920px 0px; }
.emoji-1F465 { background-position: -11940px 0px; }
.emoji-1F466 { background-position: -11960px 0px; }
.emoji-1F467 { background-position: -11980px 0px; }
.emoji-1F468 { background-position: -12000px 0px; }
.emoji-1F468-1F468-1F466 { background-position: -12020px 0px; }
.emoji-1F468-1F468-1F466-1F466 { background-position: -12040px 0px; }
.emoji-1F468-1F468-1F467 { background-position: -12060px 0px; }
.emoji-1F468-1F468-1F467-1F466 { background-position: -12080px 0px; }
.emoji-1F468-1F468-1F467-1F467 { background-position: -12100px 0px; }
.emoji-1F468-1F469-1F466-1F466 { background-position: -12120px 0px; }
.emoji-1F468-1F469-1F467 { background-position: -12140px 0px; }
.emoji-1F468-1F469-1F467-1F466 { background-position: -12160px 0px; }
.emoji-1F468-1F469-1F467-1F467 { background-position: -12180px 0px; }
.emoji-1F468-2764-1F468 { background-position: -12200px 0px; }
.emoji-1F468-2764-1F48B-1F468 { background-position: -12220px 0px; }
.emoji-1F469 { background-position: -12240px 0px; }
.emoji-1F469-1F469-1F466 { background-position: -12260px 0px; }
.emoji-1F469-1F469-1F466-1F466 { background-position: -12280px 0px; }
.emoji-1F469-1F469-1F467 { background-position: -12300px 0px; }
.emoji-1F469-1F469-1F467-1F466 { background-position: -12320px 0px; }
.emoji-1F469-1F469-1F467-1F467 { background-position: -12340px 0px; }
.emoji-1F469-2764-1F469 { background-position: -12360px 0px; }
.emoji-1F469-2764-1F48B-1F469 { background-position: -12380px 0px; }
.emoji-1F46A { background-position: -12400px 0px; }
.emoji-1F46B { background-position: -12420px 0px; }
.emoji-1F46C { background-position: -12440px 0px; }
.emoji-1F46D { background-position: -12460px 0px; }
.emoji-1F46E { background-position: -12480px 0px; }
.emoji-1F46F { background-position: -12500px 0px; }
.emoji-1F470 { background-position: -12520px 0px; }
.emoji-1F471 { background-position: -12540px 0px; }
.emoji-1F472 { background-position: -12560px 0px; }
.emoji-1F473 { background-position: -12580px 0px; }
.emoji-1F474 { background-position: -12600px 0px; }
.emoji-1F475 { background-position: -12620px 0px; }
.emoji-1F476 { background-position: -12640px 0px; }
.emoji-1F477 { background-position: -12660px 0px; }
.emoji-1F478 { background-position: -12680px 0px; }
.emoji-1F479 { background-position: -12700px 0px; }
.emoji-1F47A { background-position: -12720px 0px; }
.emoji-1F47B { background-position: -12740px 0px; }
.emoji-1F47C { background-position: -12760px 0px; }
.emoji-1F47D { background-position: -12780px 0px; }
.emoji-1F47E { background-position: -12800px 0px; }
.emoji-1F47F { background-position: -12820px 0px; }
.emoji-1F480 { background-position: -12840px 0px; }
.emoji-1F481 { background-position: -12860px 0px; }
.emoji-1F482 { background-position: -12880px 0px; }
.emoji-1F483 { background-position: -12900px 0px; }
.emoji-1F484 { background-position: -12920px 0px; }
.emoji-1F485 { background-position: -12940px 0px; }
.emoji-1F486 { background-position: -12960px 0px; }
.emoji-1F487 { background-position: -12980px 0px; }
.emoji-1F488 { background-position: -13000px 0px; }
.emoji-1F489 { background-position: -13020px 0px; }
.emoji-1F48A { background-position: -13040px 0px; }
.emoji-1F48B { background-position: -13060px 0px; }
.emoji-1F48C { background-position: -13080px 0px; }
.emoji-1F48D { background-position: -13100px 0px; }
.emoji-1F48E { background-position: -13120px 0px; }
.emoji-1F48F { background-position: -13140px 0px; }
.emoji-1F490 { background-position: -13160px 0px; }
.emoji-1F491 { background-position: -13180px 0px; }
.emoji-1F492 { background-position: -13200px 0px; }
.emoji-1F493 { background-position: -13220px 0px; }
.emoji-1F494 { background-position: -13240px 0px; }
.emoji-1F495 { background-position: -13260px 0px; }
.emoji-1F496 { background-position: -13280px 0px; }
.emoji-1F497 { background-position: -13300px 0px; }
.emoji-1F498 { background-position: -13320px 0px; }
.emoji-1F499 { background-position: -13340px 0px; }
.emoji-1F49A { background-position: -13360px 0px; }
.emoji-1F49B { background-position: -13380px 0px; }
.emoji-1F49C { background-position: -13400px 0px; }
.emoji-1F49D { background-position: -13420px 0px; }
.emoji-1F49E { background-position: -13440px 0px; }
.emoji-1F49F { background-position: -13460px 0px; }
.emoji-1F4A0 { background-position: -13480px 0px; }
.emoji-1F4A1 { background-position: -13500px 0px; }
.emoji-1F4A2 { background-position: -13520px 0px; }
.emoji-1F4A3 { background-position: -13540px 0px; }
.emoji-1F4A4 { background-position: -13560px 0px; }
.emoji-1F4A5 { background-position: -13580px 0px; }
.emoji-1F4A6 { background-position: -13600px 0px; }
.emoji-1F4A7 { background-position: -13620px 0px; }
.emoji-1F4A8 { background-position: -13640px 0px; }
.emoji-1F4A9 { background-position: -13660px 0px; }
.emoji-1F4AA { background-position: -13680px 0px; }
.emoji-1F4AB { background-position: -13700px 0px; }
.emoji-1F4AC { background-position: -13720px 0px; }
.emoji-1F4AD { background-position: -13740px 0px; }
.emoji-1F4AE { background-position: -13760px 0px; }
.emoji-1F4AF { background-position: -13780px 0px; }
.emoji-1F4B0 { background-position: -13800px 0px; }
.emoji-1F4B1 { background-position: -13820px 0px; }
.emoji-1F4B2 { background-position: -13840px 0px; }
.emoji-1F4B3 { background-position: -13860px 0px; }
.emoji-1F4B4 { background-position: -13880px 0px; }
.emoji-1F4B5 { background-position: -13900px 0px; }
.emoji-1F4B6 { background-position: -13920px 0px; }
.emoji-1F4B7 { background-position: -13940px 0px; }
.emoji-1F4B8 { background-position: -13960px 0px; }
.emoji-1F4B9 { background-position: -13980px 0px; }
.emoji-1F4BA { background-position: -14000px 0px; }
.emoji-1F4BB { background-position: -14020px 0px; }
.emoji-1F4BC { background-position: -14040px 0px; }
.emoji-1F4BD { background-position: -14060px 0px; }
.emoji-1F4BE { background-position: -14080px 0px; }
.emoji-1F4BF { background-position: -14100px 0px; }
.emoji-1F4C0 { background-position: -14120px 0px; }
.emoji-1F4C1 { background-position: -14140px 0px; }
.emoji-1F4C2 { background-position: -14160px 0px; }
.emoji-1F4C3 { background-position: -14180px 0px; }
.emoji-1F4C4 { background-position: -14200px 0px; }
.emoji-1F4C5 { background-position: -14220px 0px; }
.emoji-1F4C6 { background-position: -14240px 0px; }
.emoji-1F4C7 { background-position: -14260px 0px; }
.emoji-1F4C8 { background-position: -14280px 0px; }
.emoji-1F4C9 { background-position: -14300px 0px; }
.emoji-1F4CA { background-position: -14320px 0px; }
.emoji-1F4CB { background-position: -14340px 0px; }
.emoji-1F4CC { background-position: -14360px 0px; }
.emoji-1F4CD { background-position: -14380px 0px; }
.emoji-1F4CE { background-position: -14400px 0px; }
.emoji-1F4CF { background-position: -14420px 0px; }
.emoji-1F4D0 { background-position: -14440px 0px; }
.emoji-1F4D1 { background-position: -14460px 0px; }
.emoji-1F4D2 { background-position: -14480px 0px; }
.emoji-1F4D3 { background-position: -14500px 0px; }
.emoji-1F4D4 { background-position: -14520px 0px; }
.emoji-1F4D5 { background-position: -14540px 0px; }
.emoji-1F4D6 { background-position: -14560px 0px; }
.emoji-1F4D7 { background-position: -14580px 0px; }
.emoji-1F4D8 { background-position: -14600px 0px; }
.emoji-1F4D9 { background-position: -14620px 0px; }
.emoji-1F4DA { background-position: -14640px 0px; }
.emoji-1F4DB { background-position: -14660px 0px; }
.emoji-1F4DC { background-position: -14680px 0px; }
.emoji-1F4DD { background-position: -14700px 0px; }
.emoji-1F4DE { background-position: -14720px 0px; }
.emoji-1F4DF { background-position: -14740px 0px; }
.emoji-1F4E0 { background-position: -14760px 0px; }
.emoji-1F4E1 { background-position: -14780px 0px; }
.emoji-1F4E2 { background-position: -14800px 0px; }
.emoji-1F4E3 { background-position: -14820px 0px; }
.emoji-1F4E4 { background-position: -14840px 0px; }
.emoji-1F4E5 { background-position: -14860px 0px; }
.emoji-1F4E6 { background-position: -14880px 0px; }
.emoji-1F4E7 { background-position: -14900px 0px; }
.emoji-1F4E8 { background-position: -14920px 0px; }
.emoji-1F4E9 { background-position: -14940px 0px; }
.emoji-1F4EA { background-position: -14960px 0px; }
.emoji-1F4EB { background-position: -14980px 0px; }
.emoji-1F4EC { background-position: -15000px 0px; }
.emoji-1F4ED { background-position: -15020px 0px; }
.emoji-1F4EE { background-position: -15040px 0px; }
.emoji-1F4EF { background-position: -15060px 0px; }
.emoji-1F4F0 { background-position: -15080px 0px; }
.emoji-1F4F1 { background-position: -15100px 0px; }
.emoji-1F4F2 { background-position: -15120px 0px; }
.emoji-1F4F3 { background-position: -15140px 0px; }
.emoji-1F4F4 { background-position: -15160px 0px; }
.emoji-1F4F5 { background-position: -15180px 0px; }
.emoji-1F4F6 { background-position: -15200px 0px; }
.emoji-1F4F7 { background-position: -15220px 0px; }
.emoji-1F4F8 { background-position: -15240px 0px; }
.emoji-1F4F9 { background-position: -15260px 0px; }
.emoji-1F4FA { background-position: -15280px 0px; }
.emoji-1F4FB { background-position: -15300px 0px; }
.emoji-1F4FC { background-position: -15320px 0px; }
.emoji-1F4FD { background-position: -15340px 0px; }
.emoji-1F4FE { background-position: -15360px 0px; }
.emoji-1F500 { background-position: -15380px 0px; }
.emoji-1F501 { background-position: -15400px 0px; }
.emoji-1F502 { background-position: -15420px 0px; }
.emoji-1F503 { background-position: -15440px 0px; }
.emoji-1F504 { background-position: -15460px 0px; }
.emoji-1F505 { background-position: -15480px 0px; }
.emoji-1F506 { background-position: -15500px 0px; }
.emoji-1F507 { background-position: -15520px 0px; }
.emoji-1F508 { background-position: -15540px 0px; }
.emoji-1F509 { background-position: -15560px 0px; }
.emoji-1F50A { background-position: -15580px 0px; }
.emoji-1F50B { background-position: -15600px 0px; }
.emoji-1F50C { background-position: -15620px 0px; }
.emoji-1F50D { background-position: -15640px 0px; }
.emoji-1F50E { background-position: -15660px 0px; }
.emoji-1F50F { background-position: -15680px 0px; }
.emoji-1F510 { background-position: -15700px 0px; }
.emoji-1F511 { background-position: -15720px 0px; }
.emoji-1F512 { background-position: -15740px 0px; }
.emoji-1F513 { background-position: -15760px 0px; }
.emoji-1F514 { background-position: -15780px 0px; }
.emoji-1F515 { background-position: -15800px 0px; }
.emoji-1F516 { background-position: -15820px 0px; }
.emoji-1F517 { background-position: -15840px 0px; }
.emoji-1F518 { background-position: -15860px 0px; }
.emoji-1F519 { background-position: -15880px 0px; }
.emoji-1F51A { background-position: -15900px 0px; }
.emoji-1F51B { background-position: -15920px 0px; }
.emoji-1F51C { background-position: -15940px 0px; }
.emoji-1F51D { background-position: -15960px 0px; }
.emoji-1F51E { background-position: -15980px 0px; }
.emoji-1F51F { background-position: -16000px 0px; }
.emoji-1F520 { background-position: -16020px 0px; }
.emoji-1F521 { background-position: -16040px 0px; }
.emoji-1F522 { background-position: -16060px 0px; }
.emoji-1F523 { background-position: -16080px 0px; }
.emoji-1F524 { background-position: -16100px 0px; }
.emoji-1F525 { background-position: -16120px 0px; }
.emoji-1F526 { background-position: -16140px 0px; }
.emoji-1F527 { background-position: -16160px 0px; }
.emoji-1F528 { background-position: -16180px 0px; }
.emoji-1F529 { background-position: -16200px 0px; }
.emoji-1F52A { background-position: -16220px 0px; }
.emoji-1F52B { background-position: -16240px 0px; }
.emoji-1F52C { background-position: -16260px 0px; }
.emoji-1F52D { background-position: -16280px 0px; }
.emoji-1F52E { background-position: -16300px 0px; }
.emoji-1F52F { background-position: -16320px 0px; }
.emoji-1F530 { background-position: -16340px 0px; }
.emoji-1F531 { background-position: -16360px 0px; }
.emoji-1F532 { background-position: -16380px 0px; }
.emoji-1F533 { background-position: -16400px 0px; }
.emoji-1F534 { background-position: -16420px 0px; }
.emoji-1F535 { background-position: -16440px 0px; }
.emoji-1F536 { background-position: -16460px 0px; }
.emoji-1F537 { background-position: -16480px 0px; }
.emoji-1F538 { background-position: -16500px 0px; }
.emoji-1F539 { background-position: -16520px 0px; }
.emoji-1F53A { background-position: -16540px 0px; }
.emoji-1F53B { background-position: -16560px 0px; }
.emoji-1F53C { background-position: -16580px 0px; }
.emoji-1F53D { background-position: -16600px 0px; }
.emoji-1F546 { background-position: -16620px 0px; }
.emoji-1F547 { background-position: -16640px 0px; }
.emoji-1F548 { background-position: -16660px 0px; }
.emoji-1F549 { background-position: -16680px 0px; }
.emoji-1F54A { background-position: -16700px 0px; }
.emoji-1F550 { background-position: -16720px 0px; }
.emoji-1F551 { background-position: -16740px 0px; }
.emoji-1F552 { background-position: -16760px 0px; }
.emoji-1F553 { background-position: -16780px 0px; }
.emoji-1F554 { background-position: -16800px 0px; }
.emoji-1F555 { background-position: -16820px 0px; }
.emoji-1F556 { background-position: -16840px 0px; }
.emoji-1F557 { background-position: -16860px 0px; }
.emoji-1F558 { background-position: -16880px 0px; }
.emoji-1F559 { background-position: -16900px 0px; }
.emoji-1F55A { background-position: -16920px 0px; }
.emoji-1F55B { background-position: -16940px 0px; }
.emoji-1F55C { background-position: -16960px 0px; }
.emoji-1F55D { background-position: -16980px 0px; }
.emoji-1F55E { background-position: -17000px 0px; }
.emoji-1F55F { background-position: -17020px 0px; }
.emoji-1F560 { background-position: -17040px 0px; }
.emoji-1F561 { background-position: -17060px 0px; }
.emoji-1F562 { background-position: -17080px 0px; }
.emoji-1F563 { background-position: -17100px 0px; }
.emoji-1F564 { background-position: -17120px 0px; }
.emoji-1F565 { background-position: -17140px 0px; }
.emoji-1F566 { background-position: -17160px 0px; }
.emoji-1F567 { background-position: -17180px 0px; }
.emoji-1F568 { background-position: -17200px 0px; }
.emoji-1F569 { background-position: -17220px 0px; }
.emoji-1F56A { background-position: -17240px 0px; }
.emoji-1F56B { background-position: -17260px 0px; }
.emoji-1F56C { background-position: -17280px 0px; }
.emoji-1F56D { background-position: -17300px 0px; }
.emoji-1F56E { background-position: -17320px 0px; }
.emoji-1F56F { background-position: -17340px 0px; }
.emoji-1F570 { background-position: -17360px 0px; }
.emoji-1F571 { background-position: -17380px 0px; }
.emoji-1F572 { background-position: -17400px 0px; }
.emoji-1F573 { background-position: -17420px 0px; }
.emoji-1F574 { background-position: -17440px 0px; }
.emoji-1F575 { background-position: -17460px 0px; }
.emoji-1F576 { background-position: -17480px 0px; }
.emoji-1F577 { background-position: -17500px 0px; }
.emoji-1F578 { background-position: -17520px 0px; }
.emoji-1F579 { background-position: -17540px 0px; }
.emoji-1F57B { background-position: -17560px 0px; }
.emoji-1F57E { background-position: -17580px 0px; }
.emoji-1F57F { background-position: -17600px 0px; }
.emoji-1F581 { background-position: -17620px 0px; }
.emoji-1F582 { background-position: -17640px 0px; }
.emoji-1F583 { background-position: -17660px 0px; }
.emoji-1F585 { background-position: -17680px 0px; }
.emoji-1F586 { background-position: -17700px 0px; }
.emoji-1F587 { background-position: -17720px 0px; }
.emoji-1F588 { background-position: -17740px 0px; }
.emoji-1F589 { background-position: -17760px 0px; }
.emoji-1F58A { background-position: -17780px 0px; }
.emoji-1F58B { background-position: -17800px 0px; }
.emoji-1F58C { background-position: -17820px 0px; }
.emoji-1F58D { background-position: -17840px 0px; }
.emoji-1F58E { background-position: -17860px 0px; }
.emoji-1F58F { background-position: -17880px 0px; }
.emoji-1F590 { background-position: -17900px 0px; }
.emoji-1F591 { background-position: -17920px 0px; }
.emoji-1F592 { background-position: -17940px 0px; }
.emoji-1F593 { background-position: -17960px 0px; }
.emoji-1F594 { background-position: -17980px 0px; }
.emoji-1F595 { background-position: -18000px 0px; }
.emoji-1F596 { background-position: -18020px 0px; }
.emoji-1F597 { background-position: -18040px 0px; }
.emoji-1F598 { background-position: -18060px 0px; }
.emoji-1F599 { background-position: -18080px 0px; }
.emoji-1F59E { background-position: -18100px 0px; }
.emoji-1F59F { background-position: -18120px 0px; }
.emoji-1F5A5 { background-position: -18140px 0px; }
.emoji-1F5A6 { background-position: -18160px 0px; }
.emoji-1F5A7 { background-position: -18180px 0px; }
.emoji-1F5A8 { background-position: -18200px 0px; }
.emoji-1F5A9 { background-position: -18220px 0px; }
.emoji-1F5AA { background-position: -18240px 0px; }
.emoji-1F5AB { background-position: -18260px 0px; }
.emoji-1F5AD { background-position: -18280px 0px; }
.emoji-1F5AE { background-position: -18300px 0px; }
.emoji-1F5AF { background-position: -18320px 0px; }
.emoji-1F5B2 { background-position: -18340px 0px; }
.emoji-1F5B3 { background-position: -18360px 0px; }
.emoji-1F5B4 { background-position: -18380px 0px; }
.emoji-1F5B8 { background-position: -18400px 0px; }
.emoji-1F5B9 { background-position: -18420px 0px; }
.emoji-1F5BC { background-position: -18440px 0px; }
.emoji-1F5BD { background-position: -18460px 0px; }
.emoji-1F5BE { background-position: -18480px 0px; }
.emoji-1F5C0 { background-position: -18500px 0px; }
.emoji-1F5C1 { background-position: -18520px 0px; }
.emoji-1F5C2 { background-position: -18540px 0px; }
.emoji-1F5C3 { background-position: -18560px 0px; }
.emoji-1F5C4 { background-position: -18580px 0px; }
.emoji-1F5C6 { background-position: -18600px 0px; }
.emoji-1F5C7 { background-position: -18620px 0px; }
.emoji-1F5C9 { background-position: -18640px 0px; }
.emoji-1F5CA { background-position: -18660px 0px; }
.emoji-1F5CE { background-position: -18680px 0px; }
.emoji-1F5CF { background-position: -18700px 0px; }
.emoji-1F5D0 { background-position: -18720px 0px; }
.emoji-1F5D1 { background-position: -18740px 0px; }
.emoji-1F5D2 { background-position: -18760px 0px; }
.emoji-1F5D3 { background-position: -18780px 0px; }
.emoji-1F5D4 { background-position: -18800px 0px; }
.emoji-1F5D8 { background-position: -18820px 0px; }
.emoji-1F5D9 { background-position: -18840px 0px; }
.emoji-1F5DC { background-position: -18860px 0px; }
.emoji-1F5DD { background-position: -18880px 0px; }
.emoji-1F5DE { background-position: -18900px 0px; }
.emoji-1F5E0 { background-position: -18920px 0px; }
.emoji-1F5E1 { background-position: -18940px 0px; }
.emoji-1F5E2 { background-position: -18960px 0px; }
.emoji-1F5E3 { background-position: -18980px 0px; }
.emoji-1F5E8 { background-position: -19000px 0px; }
.emoji-1F5E9 { background-position: -19020px 0px; }
.emoji-1F5EA { background-position: -19040px 0px; }
.emoji-1F5EB { background-position: -19060px 0px; }
.emoji-1F5EC { background-position: -19080px 0px; }
.emoji-1F5ED { background-position: -19100px 0px; }
.emoji-1F5EE { background-position: -19120px 0px; }
.emoji-1F5EF { background-position: -19140px 0px; }
.emoji-1F5F0 { background-position: -19160px 0px; }
.emoji-1F5F1 { background-position: -19180px 0px; }
.emoji-1F5F2 { background-position: -19200px 0px; }
.emoji-1F5F3 { background-position: -19220px 0px; }
.emoji-1F5F4 { background-position: -19240px 0px; }
.emoji-1F5F5 { background-position: -19260px 0px; }
.emoji-1F5F8 { background-position: -19280px 0px; }
.emoji-1F5F9 { background-position: -19300px 0px; }
.emoji-1F5FA { background-position: -19320px 0px; }
.emoji-1F5FB { background-position: -19340px 0px; }
.emoji-1F5FC { background-position: -19360px 0px; }
.emoji-1F5FD { background-position: -19380px 0px; }
.emoji-1F5FE { background-position: -19400px 0px; }
.emoji-1F5FF { background-position: -19420px 0px; }
.emoji-1F600 { background-position: -19440px 0px; }
.emoji-1F601 { background-position: -19460px 0px; }
.emoji-1F602 { background-position: -19480px 0px; }
.emoji-1F603 { background-position: -19500px 0px; }
.emoji-1F604 { background-position: -19520px 0px; }
.emoji-1F605 { background-position: -19540px 0px; }
.emoji-1F606 { background-position: -19560px 0px; }
.emoji-1F607 { background-position: -19580px 0px; }
.emoji-1F608 { background-position: -19600px 0px; }
.emoji-1F609 { background-position: -19620px 0px; }
.emoji-1F60A { background-position: -19640px 0px; }
.emoji-1F60B { background-position: -19660px 0px; }
.emoji-1F60C { background-position: -19680px 0px; }
.emoji-1F60D { background-position: -19700px 0px; }
.emoji-1F60E { background-position: -19720px 0px; }
.emoji-1F60F { background-position: -19740px 0px; }
.emoji-1F610 { background-position: -19760px 0px; }
.emoji-1F611 { background-position: -19780px 0px; }
.emoji-1F612 { background-position: -19800px 0px; }
.emoji-1F613 { background-position: -19820px 0px; }
.emoji-1F614 { background-position: -19840px 0px; }
.emoji-1F615 { background-position: -19860px 0px; }
.emoji-1F616 { background-position: -19880px 0px; }
.emoji-1F617 { background-position: -19900px 0px; }
.emoji-1F618 { background-position: -19920px 0px; }
.emoji-1F619 { background-position: -19940px 0px; }
.emoji-1F61A { background-position: -19960px 0px; }
.emoji-1F61B { background-position: -19980px 0px; }
.emoji-1F61C { background-position: -20000px 0px; }
.emoji-1F61D { background-position: -20020px 0px; }
.emoji-1F61E { background-position: -20040px 0px; }
.emoji-1F61F { background-position: -20060px 0px; }
.emoji-1F620 { background-position: -20080px 0px; }
.emoji-1F621 { background-position: -20100px 0px; }
.emoji-1F622 { background-position: -20120px 0px; }
.emoji-1F623 { background-position: -20140px 0px; }
.emoji-1F624 { background-position: -20160px 0px; }
.emoji-1F625 { background-position: -20180px 0px; }
.emoji-1F626 { background-position: -20200px 0px; }
.emoji-1F627 { background-position: -20220px 0px; }
.emoji-1F628 { background-position: -20240px 0px; }
.emoji-1F629 { background-position: -20260px 0px; }
.emoji-1F62A { background-position: -20280px 0px; }
.emoji-1F62B { background-position: -20300px 0px; }
.emoji-1F62C { background-position: -20320px 0px; }
.emoji-1F62D { background-position: -20340px 0px; }
.emoji-1F62E { background-position: -20360px 0px; }
.emoji-1F62F { background-position: -20380px 0px; }
.emoji-1F630 { background-position: -20400px 0px; }
.emoji-1F631 { background-position: -20420px 0px; }
.emoji-1F632 { background-position: -20440px 0px; }
.emoji-1F633 { background-position: -20460px 0px; }
.emoji-1F634 { background-position: -20480px 0px; }
.emoji-1F635 { background-position: -20500px 0px; }
.emoji-1F636 { background-position: -20520px 0px; }
.emoji-1F637 { background-position: -20540px 0px; }
.emoji-1F638 { background-position: -20560px 0px; }
.emoji-1F639 { background-position: -20580px 0px; }
.emoji-1F63A { background-position: -20600px 0px; }
.emoji-1F63B { background-position: -20620px 0px; }
.emoji-1F63C { background-position: -20640px 0px; }
.emoji-1F63D { background-position: -20660px 0px; }
.emoji-1F63E { background-position: -20680px 0px; }
.emoji-1F63F { background-position: -20700px 0px; }
.emoji-1F640 { background-position: -20720px 0px; }
.emoji-1F641 { background-position: -20740px 0px; }
.emoji-1F642 { background-position: -20760px 0px; }
.emoji-1F645 { background-position: -20780px 0px; }
.emoji-1F646 { background-position: -20800px 0px; }
.emoji-1F647 { background-position: -20820px 0px; }
.emoji-1F648 { background-position: -20840px 0px; }
.emoji-1F649 { background-position: -20860px 0px; }
.emoji-1F64A { background-position: -20880px 0px; }
.emoji-1F64B { background-position: -20900px 0px; }
.emoji-1F64C { background-position: -20920px 0px; }
.emoji-1F64D { background-position: -20940px 0px; }
.emoji-1F64E { background-position: -20960px 0px; }
.emoji-1F64F { background-position: -20980px 0px; }
.emoji-1F680 { background-position: -21000px 0px; }
.emoji-1F681 { background-position: -21020px 0px; }
.emoji-1F682 { background-position: -21040px 0px; }
.emoji-1F683 { background-position: -21060px 0px; }
.emoji-1F684 { background-position: -21080px 0px; }
.emoji-1F685 { background-position: -21100px 0px; }
.emoji-1F686 { background-position: -21120px 0px; }
.emoji-1F687 { background-position: -21140px 0px; }
.emoji-1F688 { background-position: -21160px 0px; }
.emoji-1F689 { background-position: -21180px 0px; }
.emoji-1F68A { background-position: -21200px 0px; }
.emoji-1F68B { background-position: -21220px 0px; }
.emoji-1F68C { background-position: -21240px 0px; }
.emoji-1F68D { background-position: -21260px 0px; }
.emoji-1F68E { background-position: -21280px 0px; }
.emoji-1F68F { background-position: -21300px 0px; }
.emoji-1F690 { background-position: -21320px 0px; }
.emoji-1F691 { background-position: -21340px 0px; }
.emoji-1F692 { background-position: -21360px 0px; }
.emoji-1F693 { background-position: -21380px 0px; }
.emoji-1F694 { background-position: -21400px 0px; }
.emoji-1F695 { background-position: -21420px 0px; }
.emoji-1F696 { background-position: -21440px 0px; }
.emoji-1F697 { background-position: -21460px 0px; }
.emoji-1F698 { background-position: -21480px 0px; }
.emoji-1F699 { background-position: -21500px 0px; }
.emoji-1F69A { background-position: -21520px 0px; }
.emoji-1F69B { background-position: -21540px 0px; }
.emoji-1F69C { background-position: -21560px 0px; }
.emoji-1F69D { background-position: -21580px 0px; }
.emoji-1F69E { background-position: -21600px 0px; }
.emoji-1F69F { background-position: -21620px 0px; }
.emoji-1F6A0 { background-position: -21640px 0px; }
.emoji-1F6A1 { background-position: -21660px 0px; }
.emoji-1F6A2 { background-position: -21680px 0px; }
.emoji-1F6A3 { background-position: -21700px 0px; }
.emoji-1F6A4 { background-position: -21720px 0px; }
.emoji-1F6A5 { background-position: -21740px 0px; }
.emoji-1F6A6 { background-position: -21760px 0px; }
.emoji-1F6A7 { background-position: -21780px 0px; }
.emoji-1F6A8 { background-position: -21800px 0px; }
.emoji-1F6A9 { background-position: -21820px 0px; }
.emoji-1F6AA { background-position: -21840px 0px; }
.emoji-1F6AB { background-position: -21860px 0px; }
.emoji-1F6AC { background-position: -21880px 0px; }
.emoji-1F6AD { background-position: -21900px 0px; }
.emoji-1F6AE { background-position: -21920px 0px; }
.emoji-1F6AF { background-position: -21940px 0px; }
.emoji-1F6B0 { background-position: -21960px 0px; }
.emoji-1F6B1 { background-position: -21980px 0px; }
.emoji-1F6B2 { background-position: -22000px 0px; }
.emoji-1F6B3 { background-position: -22020px 0px; }
.emoji-1F6B4 { background-position: -22040px 0px; }
.emoji-1F6B5 { background-position: -22060px 0px; }
.emoji-1F6B6 { background-position: -22080px 0px; }
.emoji-1F6B7 { background-position: -22100px 0px; }
.emoji-1F6B8 { background-position: -22120px 0px; }
.emoji-1F6B9 { background-position: -22140px 0px; }
.emoji-1F6BA { background-position: -22160px 0px; }
.emoji-1F6BB { background-position: -22180px 0px; }
.emoji-1F6BC { background-position: -22200px 0px; }
.emoji-1F6BD { background-position: -22220px 0px; }
.emoji-1F6BE { background-position: -22240px 0px; }
.emoji-1F6BF { background-position: -22260px 0px; }
.emoji-1F6C0 { background-position: -22280px 0px; }
.emoji-1F6C1 { background-position: -22300px 0px; }
.emoji-1F6C2 { background-position: -22320px 0px; }
.emoji-1F6C3 { background-position: -22340px 0px; }
.emoji-1F6C4 { background-position: -22360px 0px; }
.emoji-1F6C5 { background-position: -22380px 0px; }
.emoji-1F6C6 { background-position: -22400px 0px; }
.emoji-1F6C7 { background-position: -22420px 0px; }
.emoji-1F6C8 { background-position: -22440px 0px; }
.emoji-1F6C9 { background-position: -22460px 0px; }
.emoji-1F6CA { background-position: -22480px 0px; }
.emoji-1F6CB { background-position: -22500px 0px; }
.emoji-1F6CC { background-position: -22520px 0px; }
.emoji-1F6CD { background-position: -22540px 0px; }
.emoji-1F6CE { background-position: -22560px 0px; }
.emoji-1F6CF { background-position: -22580px 0px; }
.emoji-1F6E0 { background-position: -22600px 0px; }
.emoji-1F6E1 { background-position: -22620px 0px; }
.emoji-1F6E2 { background-position: -22640px 0px; }
.emoji-1F6E3 { background-position: -22660px 0px; }
.emoji-1F6E4 { background-position: -22680px 0px; }
.emoji-1F6E5 { background-position: -22700px 0px; }
.emoji-1F6E6 { background-position: -22720px 0px; }
.emoji-1F6E7 { background-position: -22740px 0px; }
.emoji-1F6E8 { background-position: -22760px 0px; }
.emoji-1F6E9 { background-position: -22780px 0px; }
.emoji-1F6EA { background-position: -22800px 0px; }
.emoji-1F6EB { background-position: -22820px 0px; }
.emoji-1F6EC { background-position: -22840px 0px; }
.emoji-1F6F0 { background-position: -22860px 0px; }
.emoji-1F6F1 { background-position: -22880px 0px; }
.emoji-1F6F2 { background-position: -22900px 0px; }
.emoji-1F6F3 { background-position: -22920px 0px; }
.emoji-203C { background-position: -22940px 0px; }
.emoji-2049 { background-position: -22960px 0px; }
.emoji-2122 { background-position: -22980px 0px; }
.emoji-2139 { background-position: -23000px 0px; }
.emoji-2194 { background-position: -23020px 0px; }
.emoji-2195 { background-position: -23040px 0px; }
.emoji-2196 { background-position: -23060px 0px; }
.emoji-2197 { background-position: -23080px 0px; }
.emoji-2198 { background-position: -23100px 0px; }
.emoji-2199 { background-position: -23120px 0px; }
.emoji-21A9 { background-position: -23140px 0px; }
.emoji-21AA { background-position: -23160px 0px; }
.emoji-231A { background-position: -23180px 0px; }
.emoji-231B { background-position: -23200px 0px; }
.emoji-23E9 { background-position: -23220px 0px; }
.emoji-23EA { background-position: -23240px 0px; }
.emoji-23EB { background-position: -23260px 0px; }
.emoji-23EC { background-position: -23280px 0px; }
.emoji-23F0 { background-position: -23300px 0px; }
.emoji-23F3 { background-position: -23320px 0px; }
.emoji-24C2 { background-position: -23340px 0px; }
.emoji-25AA { background-position: -23360px 0px; }
.emoji-25AB { background-position: -23380px 0px; }
.emoji-25B6 { background-position: -23400px 0px; }
.emoji-25C0 { background-position: -23420px 0px; }
.emoji-25FB { background-position: -23440px 0px; }
.emoji-25FC { background-position: -23460px 0px; }
.emoji-25FD { background-position: -23480px 0px; }
.emoji-25FE { background-position: -23500px 0px; }
.emoji-2600 { background-position: -23520px 0px; }
.emoji-2601 { background-position: -23540px 0px; }
.emoji-260E { background-position: -23560px 0px; }
.emoji-2611 { background-position: -23580px 0px; }
.emoji-2614 { background-position: -23600px 0px; }
.emoji-2615 { background-position: -23620px 0px; }
.emoji-261D { background-position: -23640px 0px; }
.emoji-263A { background-position: -23660px 0px; }
.emoji-2648 { background-position: -23680px 0px; }
.emoji-2649 { background-position: -23700px 0px; }
.emoji-264A { background-position: -23720px 0px; }
.emoji-264B { background-position: -23740px 0px; }
.emoji-264C { background-position: -23760px 0px; }
.emoji-264D { background-position: -23780px 0px; }
.emoji-264E { background-position: -23800px 0px; }
.emoji-264F { background-position: -23820px 0px; }
.emoji-2650 { background-position: -23840px 0px; }
.emoji-2651 { background-position: -23860px 0px; }
.emoji-2652 { background-position: -23880px 0px; }
.emoji-2653 { background-position: -23900px 0px; }
.emoji-2660 { background-position: -23920px 0px; }
.emoji-2663 { background-position: -23940px 0px; }
.emoji-2665 { background-position: -23960px 0px; }
.emoji-2666 { background-position: -23980px 0px; }
.emoji-2668 { background-position: -24000px 0px; }
.emoji-267B { background-position: -24020px 0px; }
.emoji-267F { background-position: -24040px 0px; }
.emoji-2693 { background-position: -24060px 0px; }
.emoji-26A0 { background-position: -24080px 0px; }
.emoji-26A1 { background-position: -24100px 0px; }
.emoji-26AA { background-position: -24120px 0px; }
.emoji-26AB { background-position: -24140px 0px; }
.emoji-26BD { background-position: -24160px 0px; }
.emoji-26BE { background-position: -24180px 0px; }
.emoji-26C4 { background-position: -24200px 0px; }
.emoji-26C5 { background-position: -24220px 0px; }
.emoji-26CE { background-position: -24240px 0px; }
.emoji-26D4 { background-position: -24260px 0px; }
.emoji-26EA { background-position: -24280px 0px; }
.emoji-26F2 { background-position: -24300px 0px; }
.emoji-26F3 { background-position: -24320px 0px; }
.emoji-26F5 { background-position: -24340px 0px; }
.emoji-26FA { background-position: -24360px 0px; }
.emoji-26FD { background-position: -24380px 0px; }
.emoji-2702 { background-position: -24400px 0px; }
.emoji-2705 { background-position: -24420px 0px; }
.emoji-2708 { background-position: -24440px 0px; }
.emoji-2709 { background-position: -24460px 0px; }
.emoji-270A { background-position: -24480px 0px; }
.emoji-270B { background-position: -24500px 0px; }
.emoji-270C { background-position: -24520px 0px; }
.emoji-270F { background-position: -24540px 0px; }
.emoji-2712 { background-position: -24560px 0px; }
.emoji-2714 { background-position: -24580px 0px; }
.emoji-2716 { background-position: -24600px 0px; }
.emoji-2728 { background-position: -24620px 0px; }
.emoji-2733 { background-position: -24640px 0px; }
.emoji-2734 { background-position: -24660px 0px; }
.emoji-2744 { background-position: -24680px 0px; }
.emoji-2747 { background-position: -24700px 0px; }
.emoji-274C { background-position: -24720px 0px; }
.emoji-274E { background-position: -24740px 0px; }
.emoji-2753 { background-position: -24760px 0px; }
.emoji-2754 { background-position: -24780px 0px; }
.emoji-2755 { background-position: -24800px 0px; }
.emoji-2757 { background-position: -24820px 0px; }
.emoji-2764 { background-position: -24840px 0px; }
.emoji-2795 { background-position: -24860px 0px; }
.emoji-2796 { background-position: -24880px 0px; }
.emoji-2797 { background-position: -24900px 0px; }
.emoji-27A1 { background-position: -24920px 0px; }
.emoji-27B0 { background-position: -24940px 0px; }
.emoji-27BF { background-position: -24960px 0px; }
.emoji-2934 { background-position: -24980px 0px; }
.emoji-2935 { background-position: -25000px 0px; }
.emoji-2B05 { background-position: -25020px 0px; }
.emoji-2B06 { background-position: -25040px 0px; }
.emoji-2B07 { background-position: -25060px 0px; }
.emoji-2B1B { background-position: -25080px 0px; }
.emoji-2B1C { background-position: -25100px 0px; }
.emoji-2B50 { background-position: -25120px 0px; }
.emoji-2B55 { background-position: -25140px 0px; }
.emoji-3030 { background-position: -25160px 0px; }
.emoji-303D { background-position: -25180px 0px; }
.emoji-3297 { background-position: -25200px 0px; }
.emoji-3299 { background-position: -25220px 0px; }
\ No newline at end of file
......@@ -75,16 +75,15 @@
.common-note-form {
margin: 0;
background: #F7F8FA;
background: #fff;
padding: $gl-padding;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
border-top: 1px solid $border-color;
margin-bottom: -$gl-padding;
}
.note-form-actions {
background: #F9F9F9;
background: #fff;
.note-form-option {
margin-top: 8px;
......
......@@ -128,7 +128,7 @@ ul.notes {
}
&:last-child {
border-bottom: none;
border-bottom: 1px solid $border-color;
}
}
}
......
......@@ -335,6 +335,36 @@ ul.nav.nav-projects-tabs {
}
}
.top-area {
border-bottom: 1px solid #EEE;
ul.left-top-menu {
display: inline-block;
width: 50%;
margin-bottom: 0px;
border-bottom: none;
}
.projects-search-form {
width: 50%;
display: inline-block;
float: right;
padding-top: 7px;
text-align: right;
.btn-green {
margin-top: -2px;
margin-left: 10px;
}
}
@media (max-width: $screen-xs-max) {
.projects-search-form {
padding-top: 15px;
}
}
}
.fork-namespaces {
.fork-thumbnail {
text-align: center;
......@@ -412,11 +442,18 @@ pre.light-well {
.projects-search-form {
margin: -$gl-padding;
background-color: #f8fafc;
padding: $gl-padding;
margin-bottom: 0px;
border-top: 1px solid #e7e9ed;
border-bottom: 1px solid #e7e9ed;
input {
display: inline-block;
width: calc(100% - 151px);
}
.btn {
display: inline-block;
width: 135px;
}
}
.git-empty {
......
class Admin::IdentitiesController < Admin::ApplicationController
before_action :user
before_action :identity, except: :index
before_action :identity, except: [:index, :new, :create]
def new
@identity = Identity.new
end
def create
@identity = Identity.new(identity_params)
@identity.user_id = user.id
if @identity.save
redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully created.'
else
render :new
end
end
def index
@identities = @user.identities
......
......@@ -10,6 +10,7 @@ class ApplicationController < ActionController::Base
before_action :authenticate_user_from_token!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
before_action :reject_blocked!
before_action :check_password_expiration
before_action :ldap_security_check
......@@ -202,6 +203,20 @@ class ApplicationController < ActionController::Base
end
end
def validate_user_service_ticket!
return unless signed_in? && session[:service_tickets]
valid = session[:service_tickets].all? do |provider, ticket|
Gitlab::OAuth::Session.valid?(provider, ticket)
end
unless valid
session[:service_tickets] = nil
sign_out current_user
redirect_to new_user_session_path
end
end
def check_password_expiration
if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user?
redirect_to new_profile_password_path and return
......
......@@ -19,8 +19,10 @@ module Ci
@error = e.message
@status = false
rescue
@error = "Undefined error"
@error = 'Undefined error'
@status = false
ensure
render :show
end
end
end
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
protect_from_forgery except: [:kerberos, :saml]
protect_from_forgery except: [:kerberos, :saml, :cas3]
Gitlab.config.omniauth.providers.each do |provider|
define_method provider['name'] do
......@@ -42,6 +42,14 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
render 'errors/omniauth_error', layout: "errors", status: 422
end
def cas3
ticket = params['ticket']
if ticket
handle_service_ticket oauth['provider'], ticket
end
handle_omniauth
end
private
def handle_omniauth
......@@ -84,6 +92,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
redirect_to new_user_session_path
end
def handle_service_ticket provider, ticket
Gitlab::OAuth::Session.create provider, ticket
session[:service_tickets] ||= {}
session[:service_tickets][provider] = ticket
end
def oauth
@oauth ||= request.env['omniauth.auth']
end
......
......@@ -139,7 +139,6 @@ class Projects::NotesController < Projects::ApplicationController
discussion_id: note.discussion_id,
html: note_to_html(note),
award: note.is_award,
emoji_path: note.is_award ? view_context.image_url(::AwardEmoji.path_to_emoji_image(note.note)) : "",
note: note.note,
discussion_html: note_to_discussion_html(note),
discussion_with_diff_html: note_to_discussion_with_diff_html(note)
......
class Projects::ServicesController < Projects::ApplicationController
ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_version, :subdomain,
ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_url, :api_version, :subdomain,
:room, :recipients, :project_url, :webhook,
:user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
:build_key, :server, :teamcity_url, :drone_url, :build_type,
......@@ -10,7 +10,8 @@ class Projects::ServicesController < Projects::ApplicationController
:notify_only_broken_builds, :add_pusher,
:send_from_committer_email, :disable_diffs, :external_wiki_url,
:notify, :color,
:server_host, :server_port, :default_irc_uri, :enable_ssl_verification]
:server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
:jira_issue_transition_id]
# Parameters to ignore if no value is specified
FILTER_BLANK_PARAMS = [:password]
......
......@@ -98,11 +98,13 @@ module IssuesHelper
end.sort.to_sentence(last_word_connector: ', or ')
end
def url_to_emoji(name)
emoji_path = ::AwardEmoji.path_to_emoji_image(name)
url_to_image(emoji_path)
rescue StandardError
""
def emoji_icon(name, unicode = nil)
unicode ||= Emoji.emoji_filename(name)
content_tag :div, "",
class: "icon emoji-icon emoji-#{unicode}",
"data-emoji" => name,
"data-unicode-name" => unicode
end
def emoji_author_list(notes, current_user)
......@@ -113,10 +115,6 @@ module IssuesHelper
list.join(", ")
end
def emoji_list
::AwardEmoji::EMOJI_LIST
end
def note_active_class(notes, current_user)
if current_user && notes.pluck(:author_id).include?(current_user.id)
"active"
......
......@@ -27,7 +27,16 @@ module MergeRequestsHelper
end
def ci_build_details_path(merge_request)
merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch)
build_url = merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch)
return nil unless build_url
parsed_url = URI.parse(build_url)
unless parsed_url.userinfo.blank?
parsed_url.userinfo = ''
end
parsed_url.to_s
end
def merge_path_description(merge_request, separator)
......
......@@ -105,6 +105,14 @@ module ProjectsHelper
end
end
def user_max_access_in_project(user_id, project)
level = project.team.max_member_access(user_id)
if level
Gitlab::Access.options_with_owner.key(level)
end
end
private
def get_project_nav_tabs(project, current_user)
......@@ -277,14 +285,6 @@ module ProjectsHelper
end
end
def user_max_access_in_project(user, project)
level = project.team.max_member_access(user)
if level
Gitlab::Access.options_with_owner.key(level)
end
end
def leave_project_message(project)
"Are you sure you want to leave \"#{project.name}\" project?"
end
......
......@@ -134,4 +134,8 @@ class ApplicationSetting < ActiveRecord::Base
/x)
self.restricted_signup_domains.reject! { |d| d.empty? }
end
def runners_registration_token
ensure_runners_registration_token!
end
end
......@@ -135,6 +135,16 @@ module Ci
predefined_variables + yaml_variables + project_variables + trigger_variables
end
def merge_request
merge_requests = MergeRequest.includes(:merge_request_diff)
.where(source_branch: ref, source_project_id: commit.gl_project_id)
.reorder(iid: :asc)
merge_requests.find do |merge_request|
merge_request.commits.any? { |ci| ci.id == commit.sha }
end
end
def project
commit.project
end
......@@ -170,7 +180,8 @@ module Ci
def extract_coverage(text, regex)
begin
matches = text.gsub(Regexp.new(regex)).to_a.last
matches = text.scan(Regexp.new(regex)).last
matches = matches.last if matches.kind_of?(Array)
coverage = matches.gsub(/\d+(\.\d+)?/).first
if coverage.present?
......
......@@ -37,7 +37,7 @@ module Participable
# Be aware that this method makes a lot of sql queries.
# Save result into variable if you are going to reuse it inside same request
def participants(current_user = self.author, load_lazy_references: true)
def participants(current_user = self.author)
participants =
Gitlab::ReferenceExtractor.lazily do
self.class.participant_attrs.flat_map do |attr|
......
......@@ -18,15 +18,16 @@ module TokenAuthenticatable
define_method("ensure_#{token_field}") do
current_token = read_attribute(token_field)
if current_token.blank?
write_attribute(token_field, generate_token_for(token_field))
else
current_token
current_token.blank? ? write_new_token(token_field) : current_token
end
define_method("ensure_#{token_field}!") do
send("reset_#{token_field}!") if read_attribute(token_field).blank?
read_attribute(token_field)
end
define_method("reset_#{token_field}!") do
write_attribute(token_field, generate_token_for(token_field))
write_new_token(token_field)
save!
end
end
......@@ -34,7 +35,12 @@ module TokenAuthenticatable
private
def generate_token_for(token_field)
def write_new_token(token_field)
new_token = generate_token(token_field)
write_attribute(token_field, new_token)
end
def generate_token(token_field)
loop do
token = Devise.friendly_token
break token unless self.class.unscoped.find_by(token_field => token)
......
......@@ -86,7 +86,7 @@ class Issue < ActiveRecord::Base
def referenced_merge_requests
Gitlab::ReferenceExtractor.lazily do
[self, *notes].flat_map do |note|
note.all_references(load_lazy_references: false).merge_requests
note.all_references.merge_requests
end
end.sort_by(&:iid)
end
......
class JiraIssue < ExternalIssue
end
......@@ -335,7 +335,7 @@ class MergeRequest < ActiveRecord::Base
issues = commits.flat_map { |c| c.closes_issues(current_user) }
issues.push(*Gitlab::ClosingIssueExtractor.new(project, current_user).
closed_by_message(description))
issues.uniq
issues.uniq(&:id)
else
[]
end
......
......@@ -499,6 +499,10 @@ class Project < ActiveRecord::Base
@ci_service ||= ci_services.find(&:activated?)
end
def jira_tracker?
issues_tracker.to_param == 'jira'
end
def avatar_type
unless self.avatar.image?
self.errors.add :avatar, 'only images allowed'
......@@ -799,6 +803,10 @@ class Project < ActiveRecord::Base
false
end
def jira_tracker_active?
jira_tracker? && jira_service.active
end
def ci_commit(sha)
ci_commits.find_by(sha: sha)
end
......
......@@ -18,6 +18,11 @@
# note_events :boolean default(TRUE), not null
#
# TODO(ayufan): The GitLabCiService is deprecated and the type should be removed when the database entries are removed
class GitlabCiService < CiService
# this is no longer used
# We override the active accessor to always make GitLabCiService disabled
# Otherwise the GitLabCiService can be picked, but should never be since it's deprecated
def active
false
end
end
......@@ -19,9 +19,24 @@
#
class JiraService < IssueTrackerService
include HTTParty
include Gitlab::Application.routes.url_helpers
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
DEFAULT_API_VERSION = 2
prop_accessor :username, :password, :api_url, :jira_issue_transition_id,
:title, :description, :project_url, :issues_url, :new_issue_url
before_validation :set_api_url, :set_jira_issue_transition_id
before_update :reset_password
def reset_password
# don't reset the password if a new one is provided
if api_url_changed? && !password_touched?
self.password = nil
end
end
def help
line1 = 'Setting `project_url`, `issues_url` and `new_issue_url` will '\
......@@ -54,4 +69,228 @@ class JiraService < IssueTrackerService
def to_param
'jira'
end
def fields
super.push(
{ type: 'text', name: 'api_url', placeholder: 'https://jira.example.com/rest/api/2' },
{ type: 'text', name: 'username', placeholder: '' },
{ type: 'password', name: 'password', placeholder: '' },
{ type: 'text', name: 'jira_issue_transition_id', placeholder: '2' }
)
end
def execute(push, issue = nil)
if issue.nil?
# No specific issue, that means
# we just want to test settings
test_settings
else
close_issue(push, issue)
end
end
def create_cross_reference_note(mentioned, noteable, author)
issue_name = mentioned.id
project = self.project
noteable_name = noteable.class.name.underscore.downcase
noteable_id = if noteable.is_a?(Commit)
noteable.id
else
noteable.iid
end
entity_url = build_entity_url(noteable_name.to_sym, noteable_id)
data = {
user: {
name: author.name,
url: resource_url(user_path(author)),
},
project: {
name: project.path_with_namespace,
url: resource_url(namespace_project_path(project.namespace, project))
},
entity: {
name: noteable_name.humanize.downcase,
url: entity_url
}
}
add_comment(data, issue_name)
end
def test_settings
result = JiraService.get(
jira_api_test_url,
headers: {
'Content-Type' => 'application/json',
'Authorization' => "Basic #{auth}"
}
)
case result.code
when 201, 200
Rails.logger.info("#{self.class.name} SUCCESS #{result.code}: Successfully connected to #{api_url}.")
true
else
Rails.logger.info("#{self.class.name} ERROR #{result.code}: #{result.parsed_response}")
false
end
rescue Errno::ECONNREFUSED => e
Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{api_url}."
false
end
private
def build_api_url_from_project_url
server = URI(project_url)
default_ports = [["http",80],["https",443]].include?([server.scheme,server.port])
server_url = "#{server.scheme}://#{server.host}"
server_url.concat(":#{server.port}") unless default_ports
"#{server_url}/rest/api/#{DEFAULT_API_VERSION}"
rescue
"" # looks like project URL was not valid
end
def set_api_url
self.api_url = build_api_url_from_project_url if self.api_url.blank?
end
def set_jira_issue_transition_id
self.jira_issue_transition_id ||= "2"
end
def close_issue(entity, issue)
commit_id = if entity.is_a?(Commit)
entity.id
elsif entity.is_a?(MergeRequest)
entity.last_commit.id
end
commit_url = build_entity_url(:commit, commit_id)
# Depending on the JIRA project's workflow, a comment during transition
# may or may not be allowed. Split the operation in to two calls so the
# comment always works.
transition_issue(issue)
add_issue_solved_comment(issue, commit_id, commit_url)
end
def transition_issue(issue)
message = {
transition: {
id: jira_issue_transition_id
}
}
send_message(close_issue_url(issue.iid), message.to_json)
end
def add_issue_solved_comment(issue, commit_id, commit_url)
comment = {
body: "Issue solved with [#{commit_id}|#{commit_url}]."
}
send_message(comment_url(issue.iid), comment.to_json)
end
def add_comment(data, issue_name)
url = comment_url(issue_name)
user_name = data[:user][:name]
user_url = data[:user][:url]
entity_name = data[:entity][:name]
entity_url = data[:entity][:url]
project_name = data[:project][:name]
message = {
body: "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]."
}
unless existing_comment?(issue_name, message[:body])
send_message(url, message.to_json)
end
end
def auth
require 'base64'
Base64.urlsafe_encode64("#{self.username}:#{self.password}")
end
def send_message(url, message)
result = JiraService.post(
url,
body: message,
headers: {
'Content-Type' => 'application/json',
'Authorization' => "Basic #{auth}"
}
)
message = case result.code
when 201, 200, 204
"#{self.class.name} SUCCESS #{result.code}: Successfully posted to #{url}."
when 401
"#{self.class.name} ERROR 401: Unauthorized. Check the #{self.username} credentials and JIRA access permissions and try again."
else
"#{self.class.name} ERROR #{result.code}: #{result.parsed_response}"
end
Rails.logger.info(message)
message
rescue URI::InvalidURIError, Errno::ECONNREFUSED => e
Rails.logger.info "#{self.class.name} ERROR: #{e.message}. Hostname: #{url}."
end
def existing_comment?(issue_name, new_comment)
result = JiraService.get(
comment_url(issue_name),
headers: {
'Content-Type' => 'application/json',
'Authorization' => "Basic #{auth}"
}
)
case result.code
when 201, 200
existing_comments = JSON.parse(result.body)['comments']
if existing_comments.present?
return existing_comments.map { |comment| comment['body'].include?(new_comment) }.any?
end
end
false
rescue JSON::ParserError
false
end
def resource_url(resource)
"#{Settings.gitlab['url'].chomp("/")}#{resource}"
end
def build_entity_url(entity_name, entity_id)
resource_url(
polymorphic_url(
[
self.project.namespace.becomes(Namespace),
self.project,
entity_name
],
id: entity_id,
routing_type: :path
)
)
end
def close_issue_url(issue_name)
"#{self.api_url}/issue/#{issue_name}/transitions"
end
def comment_url(issue_name)
"#{self.api_url}/issue/#{issue_name}/comment"
end
def jira_api_test_url
"#{self.api_url}/myself"
end
end
......@@ -26,6 +26,7 @@
# bio :string(255)
# failed_attempts :integer default(0)
# locked_at :datetime
# unlock_token :string(255)
# username :string(255)
# can_create_group :boolean default(TRUE), not null
# can_create_team :boolean default(TRUE), not null
......
module Issues
class CloseService < Issues::BaseService
def execute(issue, commit = nil)
if project.jira_tracker? && project.jira_service.active
project.jira_service.execute(commit, issue)
return issue
end
if project.default_issues_tracker? && issue.close
event_service.close_issue(issue, current_user)
create_note(issue, commit)
......
......@@ -241,8 +241,13 @@ class SystemNoteService
note_options.merge!(noteable: noteable)
end
if noteable.is_a?(ExternalIssue)
noteable.project.issues_tracker.create_cross_reference_note(noteable, mentioner, author)
else
create_note(note_options)
end
end
def self.cross_reference?(note_text)
note_text.start_with?(cross_reference_note_prefix)
......@@ -259,7 +264,7 @@ class SystemNoteService
#
# Returns Boolean
def self.cross_reference_disallowed?(noteable, mentioner)
return true if noteable.is_a?(ExternalIssue)
return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active?
return false unless mentioner.is_a?(MergeRequest)
return false unless noteable.is_a?(Commit)
......
......@@ -79,6 +79,10 @@
GitLab API
%span.pull-right
= API::API::version
%p
Git
%span.pull-right
= Gitlab::Git.version
%p
Ruby
%span.pull-right
......
- page_title "Identities", @user.name, "Users"
= render 'admin/users/head'
= link_to 'New Identity', new_admin_user_identity_path, class: 'pull-right btn btn-new'
- if @identities.present?
.table-holder
%table.table
......
- page_title "New Identity"
%h3.page-title New identity
%hr
= render 'form'
......@@ -3,7 +3,7 @@
To register a new runner you should enter the following registration token.
With this token the runner will request a unique runner token and use that for future communication.
Registration token is
%code{ id: 'runners-token' } #{current_application_settings.ensure_runners_registration_token}
%code{ id: 'runners-token' } #{current_application_settings.runners_registration_token}
.bs-callout.clearfix
.pull-left
......
......@@ -41,5 +41,3 @@
%i.fa.fa-remove.incorrect-syntax
%b Error:
= @error
:plain
$(".results").html("#{escape_javascript(render "create")}")
\ No newline at end of file
%h2 Check your .gitlab-ci.yml
%hr
= form_tag ci_lint_path, method: :post, remote: true do
.control-group
= label_tag :content, "Content of .gitlab-ci.yml", class: 'control-label'
.controls
.row
= form_tag ci_lint_path, method: :post do
.form-group
= label_tag :content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap'
.col-sm-12
= text_area_tag :content, nil, class: 'form-control span1', rows: 7, require: true
.col-sm-12
.pull-left.prepend-top-10
= submit_tag 'Validate', class: 'btn btn-success submit-yml'
.control-group.clearfix
.controls.pull-left.prepend-top-10
= submit_tag "Validate", class: 'btn btn-success submit-yml'
%p.text-center.loading
%i.fa.fa-refresh.fa-spin
.results.prepend-top-20
:javascript
$(".loading").hide();
$('form').bind('ajax:beforeSend', function() {
$(".loading").show();
});
$('form').bind('ajax:complete', function() {
$(".loading").hide();
});
.row.prepend-top-20
.col-sm-12
.results
= render partial: 'create' if defined?(@status)
= content_for :flash_message do
= render 'shared/project_limit'
%ul.center-top-menu
.top-area
%ul.left-top-menu
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
......@@ -11,3 +11,10 @@
= nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path], html_options: { class: 'hidden-xs' }) do
= link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do
Explore Projects
.projects-search-form
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name...', class: 'projects-list-filter form-control hidden-xs', spellcheck: false
- if current_user.can_create_project?
= link_to new_project_path, class: 'btn btn-green' do
%i.fa.fa-plus
New Project
.projects-list-holder
.projects-search-form
.input-group
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
- if current_user.can_create_project?
%span.input-group-btn
= link_to new_project_path, class: 'btn btn-green' do
%i.fa.fa-plus
New Project
= render 'shared/projects/list', projects: @projects, ci: true
<p>Hello <%= @resource.email %>!</p>
<p>Your account has been locked due to an excessive amount of unsuccessful sign in attempts.</p>
<p>Click the link below to unlock your account:</p>
<p><%= link_to 'Unlock your account', unlock_url(@resource, unlock_token: @token) %></p>
%p
Hello #{@resource.name}!
%p
Your GitLab account has been locked due to an excessive amount of unsuccessful
sign in attempts. Your account will automatically unlock in
= time_ago_in_words(Devise.unlock_in.from_now)
or you may click the link below to unlock now.
%p= link_to 'Unlock your account', unlock_url(@resource, unlock_token: @token)
<h2>Resend unlock instructions</h2>
<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
<%= devise_error_messages! %>
<div><%= f.label :email %><br />
<%= f.email_field :email %></div>
<div><%= f.submit "Resend unlock instructions" %></div>
<% end %>
<%= render partial: "devise/shared/links" %>
.login-box
.login-heading
%h3 Resend unlock email
.login-body
= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f|
.devise-errors
= devise_error_messages!
.clearfix.append-bottom-20
= f.email_field :email, class: 'form-control', placeholder: 'Email', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off'
.clearfix
= f.submit 'Resend unlock instructions', class: 'btn btn-success'
.clearfix.prepend-top-20
= render 'devise/shared/sign_in_link'
.panel.panel-default.projects-list-holder
.panel-heading.clearfix
.projects-list-holder
.projects-search-form
.input-group
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
- if can? current_user, :create_projects, @group
......
......@@ -5,24 +5,33 @@
- if current_user
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
.dashboard
.header-with-avatar.clearfix
.cover-block
.avatar-holder
= link_to group_icon(@group), target: '_blank' do
= image_tag group_icon(@group), class: "avatar group-avatar s90"
%h3
.cover-title
= @group.name
.username
.cover-desc.username
@#{@group.path}
- if @group.description.present?
.description
.cover-desc.description
= markdown(@group.description, pipeline: :description)
%hr
= render 'shared/show_aside'
- if can?(current_user, :read_group, @group)
%ul.center-top-menu.no-top
%li.active
= link_to "#activity", 'data-toggle' => 'tab' do
Activity
- if @projects.present?
%li
= link_to "#projects", 'data-toggle' => 'tab' do
Projects
- if can?(current_user, :read_group, @group)
.row
%section.activities.col-md-7
.hidden-xs
.tab-content
.tab-pane.active#activity
.gray-content-block.activity-filter-block
- if current_user
= render "events/event_last_push", event: @last_push
.pull-right
......@@ -30,12 +39,13 @@
%i.fa.fa-rss
= render 'shared/event_filter'
%hr
.content_list
= spinner
%aside.side.col-md-5
.tab-pane#projects
= render "projects", projects: @projects
- else
- else
%p
This group does not have public projects
......@@ -7,6 +7,10 @@
%strong.monospace= link_to @build.commit.short_sha, ci_status_path(@build.commit)
from
= link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref)
- merge_request = @build.merge_request
- if merge_request
via
= link_to "merge request ##{merge_request.iid}", merge_request_path(merge_request)
#up-build-trace
- if @commit.matrix_for_ref?(@build.ref)
......
- content_for :note_actions do
- if can?(current_user, :update_issue, @issue)
- if @issue.closed?
= link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-grouped btn-reopen js-note-target-reopen', title: 'Reopen Issue'
= link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-reopen js-note-target-reopen', title: 'Reopen Issue'
- else
= link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close js-note-target-close', title: 'Close Issue'
= link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-close js-note-target-close', title: 'Close Issue'
#notes
= render 'projects/notes/notes_with_form'
......@@ -20,14 +20,14 @@
.pull-right
- if can?(current_user, :create_issue, @project)
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-grouped new-issue-link', title: 'New Issue', id: 'new_issue_link' do
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New Issue', id: 'new_issue_link' do
= icon('plus')
New Issue
- if can?(current_user, :update_issue, @issue)
= link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen Issue'
= link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close Issue'
= link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen Issue'
= link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close Issue'
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-grouped issuable-edit' do
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-nr btn-grouped issuable-edit' do
= icon('pencil-square-o')
Edit
......
- content_for :note_actions do
- if can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.open?
= link_to 'Close', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request"
= link_to 'Close', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request"
- if @merge_request.closed?
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request"
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request"
#notes= render "projects/notes/notes_with_form"
......@@ -17,9 +17,9 @@
.issue-btn-group.pull-right
- if can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.open?
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: 'btn btn-grouped btn-close', title: 'Close merge request'
= link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-grouped issuable-edit', id: 'edit_merge_request' do
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: 'btn btn-nr btn-grouped btn-close', title: 'Close merge request'
= link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-nr btn-grouped issuable-edit', id: 'edit_merge_request' do
%i.fa.fa-pencil-square-o
Edit
- if @merge_request.closed?
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'btn btn-grouped btn-reopen reopen-mr-link', title: 'Reopen merge request'
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'btn btn-nr btn-grouped btn-reopen reopen-mr-link', title: 'Reopen merge request'
......@@ -13,6 +13,6 @@
.error-alert
.note-form-actions.clearfix
= f.submit 'Add Comment', class: "btn btn-create comment-btn btn-grouped js-comment-button"
= f.submit 'Add Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button"
= yield(:note_actions)
%a.btn.btn-cancel.js-close-discussion-note-form Cancel
......@@ -71,7 +71,7 @@
= render default_project_view
- if current_user
- access = user_max_access_in_project(current_user, @project)
- access = user_max_access_in_project(current_user.id, @project)
- if access
.prepend-top-20.project-footer
.gray-content-block.footer-block.center
......
.awards.votes-block
- votable.notes.awards.grouped_awards.each do |emoji, notes|
.award{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user)}
.icon{"data-emoji" => "#{emoji}"}
= image_tag url_to_emoji(emoji), height: "20px", width: "20px"
= emoji_icon(emoji)
.counter
= notes.count
- if current_user
.dropdown.awards-controls
.awards-controls
%a.add-award{"data-toggle" => "dropdown", "data-target" => "#", "href" => "#"}
= icon('smile-o')
%ul.dropdown-menu.awards-menu
- emoji_list.each do |emoji|
%li{"data-emoji" => "#{emoji}"}= image_tag url_to_emoji(emoji), height: "20px", width: "20px"
.emoji-menu
.emoji-menu-content
- AwardEmoji.emoji_by_category.each do |category, emojis|
%h5= AwardEmoji::CATEGORIES[category]
%ul
- emojis.each do |emoji|
%li
= emoji_icon(emoji["name"], emoji["unicode"])
- if current_user
:coffeescript
......@@ -20,10 +24,16 @@
noteable_type = "#{votable.class.name.underscore}"
noteable_id = "#{votable.id}"
aliases = #{AwardEmoji::ALIASES.to_json}
window.awards_handler = new AwardsHandler(post_emoji_url, noteable_type, noteable_id, aliases)
$(".awards-menu li").click (e)->
emoji = $(this).data("emoji")
window.awards_handler = new AwardsHandler(
post_emoji_url,
noteable_type,
noteable_id,
aliases
)
$(".emoji-menu-content li").click (e)->
emoji = $(this).find(".emoji-icon").data("emoji")
awards_handler.addAward(emoji)
$(".awards").on "click", ".award", (e)->
......@@ -31,3 +41,5 @@
awards_handler.addAward(emoji)
$(".award").tooltip()
$(".emoji-menu-content").niceScroll({cursorwidth: "7px", autohidemode: false})
......@@ -144,6 +144,15 @@ production: &base
# plain_url: "http://..." # default: http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
# ssl_url: "https://..." # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
## Auxiliary jobs
# Periodically executed jobs, to self-heal Gitlab, do external synchronizations, etc.
# Please read here for more information: https://github.com/ondrejbartas/sidekiq-cron#adding-cron-job
cron_jobs:
# Flag stuck CI builds as failed
stuck_ci_builds_worker:
cron: "0 0 * * *"
#
# 2. GitLab CI settings
# ==========================
......@@ -287,6 +296,15 @@ production: &base
# arguments, followed by optional 'args' which can be either a hash or an array.
# Documentation for this is available at http://doc.gitlab.com/ce/integration/omniauth.html
providers:
# See omniauth-cas3 for more configuration details
# - { name: 'cas3',
# label: 'cas3',
# args: {
# url: 'https://sso.example.com',
# disable_ssl_verification: false,
# login_url: '/cas/login',
# service_validate_url: '/cas/p3/serviceValidate',
# logout_url: '/cas/logout'} }
# - { name: 'github',
# app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET',
......@@ -324,6 +342,10 @@ production: &base
# application_name: 'YOUR_APP_NAME',
# application_password: 'YOUR_APP_PASSWORD' } }
# SSO maximum session duration in seconds. Defaults to CAS default of 8 hours.
# cas3:
# session_duration: 28800
# Shared file storage settings
shared:
# path: /mnt/gitlab # Default: shared
......
......@@ -126,6 +126,10 @@ Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block
Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil?
Settings.omniauth['providers'] ||= []
Settings.omniauth['cas3'] ||= Settingslogic.new({})
Settings.omniauth.cas3['session_duration'] ||= 8.hours
Settings.omniauth['session_tickets'] ||= Settingslogic.new({})
Settings.omniauth.session_tickets['cas3'] = 'ticket'
Settings['shared'] ||= Settingslogic.new({})
Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root)
......@@ -164,7 +168,7 @@ Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].
Settings.gitlab['twitter_sharing_enabled'] ||= true if Settings.gitlab['twitter_sharing_enabled'].nil?
Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], [])
Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil?
Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)' if Settings.gitlab['issue_closing_pattern'].nil?
Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z]*-\d*))+)' if Settings.gitlab['issue_closing_pattern'].nil?
Settings.gitlab['default_projects_features'] ||= {}
Settings.gitlab['webhook_timeout'] ||= 10
Settings.gitlab['max_attachment_size'] ||= 10
......@@ -224,6 +228,15 @@ Settings.gravatar['plain_url'] ||= 'http://www.gravatar.com/avatar/%{hash}?s=%{
Settings.gravatar['ssl_url'] ||= 'https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon'
Settings.gravatar['host'] = Settings.get_host_without_www(Settings.gravatar['plain_url'])
#
# Cron Jobs
#
Settings['cron_jobs'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_ci_builds_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_ci_builds_worker']['cron'] ||= '0 0 * * *'
Settings.cron_jobs['stuck_ci_builds_worker']['job_class'] = 'StuckCiBuildsWorker'
#
# GitLab Shell
#
......
......@@ -121,14 +121,14 @@ Devise.setup do |config|
config.lock_strategy = :failed_attempts
# Defines which key will be used when locking and unlocking an account
# config.unlock_keys = [ :email ]
config.unlock_keys = [ :email ]
# Defines which strategy will be used to unlock an account.
# :email = Sends an unlock link to the user email
# :time = Re-enables login after a certain amount of time (see :unlock_in below)
# :both = Enables both strategies
# :none = No unlock strategy. You should handle unlocking by yourself.
config.unlock_strategy = :time
config.unlock_strategy = :both
# Number of authentication tries before locking an account if lock_strategy
# is failed attempts.
......@@ -241,6 +241,16 @@ Devise.setup do |config|
# An Array from the configuration will be expanded.
provider_arguments.concat provider['args']
when Hash
# Add procs for handling SLO
if provider['name'] == 'cas3'
provider['args'][:on_single_sign_out] = lambda do |request|
ticket = request.params[:session_index]
raise "Service Ticket not found." unless Gitlab::OAuth::Session.valid?(:cas3, ticket)
Gitlab::OAuth::Session.destroy(:cas3, ticket)
true
end
end
# A Hash from the configuration will be passed as is.
provider_arguments << provider['args'].symbolize_keys
end
......
......@@ -18,11 +18,12 @@ Sidekiq.configure_server do |config|
chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS']
end
# Sidekiq-cron: load recurring jobs from schedule.yml
schedule_file = 'config/schedule.yml'
if File.exists?(schedule_file)
Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file)
end
# Sidekiq-cron: load recurring jobs from gitlab.yml
# UGLY Hack to get nested hash from settingslogic
cron_jobs = JSON.parse(Gitlab.config.cron_jobs.to_json)
# UGLY hack: Settingslogic doesn't allow 'class' key
cron_jobs.each { |k,v| cron_jobs[k]['class'] = cron_jobs[k].delete('job_class') }
Sidekiq::Cron::Job.load_from_hash! cron_jobs
# Database pool should be at least `sidekiq_concurrency` + 2
# For more info, see: https://github.com/mperham/sidekiq/blob/master/4.0-Upgrade.md
......
......@@ -188,7 +188,7 @@ Rails.application.routes.draw do
namespace :admin do
resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do
resources :keys, only: [:show, :destroy]
resources :identities, only: [:index, :edit, :update, :destroy]
resources :identities, except: [:show]
delete 'stop_impersonation' => 'impersonation#destroy', on: :collection
......
# Here is a list of jobs that are scheduled to run periodically.
# We use a UNIX cron notation to specify execution schedule.
#
# Please read here for more information:
# https://github.com/ondrejbartas/sidekiq-cron#adding-cron-job
stuck_ci_builds_worker:
cron: "0 0 * * *"
class: "StuckCiBuildsWorker"
queue: "default"
class SetJiraServiceApiUrl < ActiveRecord::Migration
# This migration can be performed online without errors, but some Jira API calls may be missed
# when doing so because api_url is not yet available.
def build_api_url_from_project_url(project_url, api_version)
# this is the exact logic previously used to build the Jira API URL from project_url
server = URI(project_url)
default_ports = [80, 443].include?(server.port)
server_url = "#{server.scheme}://#{server.host}"
server_url.concat(":#{server.port}") unless default_ports
"#{server_url}/rest/api/#{api_version}"
end
def get_api_version_from_api_url(api_url)
match = /\/rest\/api\/(?<api_version>\w+)$/.match(api_url)
match && match['api_version']
end
def change
reversible do |dir|
select_all("SELECT id, properties FROM services WHERE services.type IN ('JiraService')").each do |jira_service|
id = jira_service["id"]
properties = JSON.parse(jira_service["properties"])
properties_was = properties.clone
dir.up do
# remove api_version and set api_url
if properties['api_version'].present? && properties['project_url'].present?
begin
properties['api_url'] ||= build_api_url_from_project_url(properties['project_url'], properties['api_version'])
rescue
# looks like project_url was not a valid URL. Do nothing.
end
end
properties.delete('api_version') if properties.include?('api_version')
end
dir.down do
# remove api_url and set api_version (default to '2')
properties['api_version'] ||= get_api_version_from_api_url(properties['api_url']) || '2'
properties.delete('api_url') if properties.include?('api_url')
end
if properties != properties_was
execute("UPDATE services SET properties = '#{quote_string(properties.to_json)}' WHERE id = #{id}")
end
end
end
end
end
class AddBuildEventsToServices < ActiveRecord::Migration
def up
def change
add_column :services, :build_events, :boolean, default: false, null: false
add_column :web_hooks, :build_events, :boolean, default: false, null: false
end
......
......@@ -10,4 +10,7 @@ class MigrateCiWebHooks < ActiveRecord::Migration
'JOIN projects ON ci_projects.gitlab_id = projects.id'
)
end
def down
end
end
class AddUnlockTokenToUser < ActiveRecord::Migration
def change
add_column :users, :unlock_token, :string
end
end
class AddCiToProject < ActiveRecord::Migration
def up
def change
add_column :projects, :ci_id, :integer
add_column :projects, :builds_enabled, :boolean, default: true, null: false
add_column :projects, :shared_runners_enabled, :boolean, default: true, null: false
......
class AddProjectIdToCi < ActiveRecord::Migration
def up
def change
add_column :ci_builds, :gl_project_id, :integer
add_column :ci_runner_projects, :gl_project_id, :integer
add_column :ci_triggers, :gl_project_id, :integer
......
......@@ -14,6 +14,10 @@ class MigrateCiToProject < ActiveRecord::Migration
migrate_ci_service
end
def down
# We can't reverse the data
end
def migrate_project_id_for_table(table)
subquery = "SELECT gitlab_id FROM ci_projects WHERE ci_projects.id = #{table}.project_id"
execute("UPDATE #{table} SET gl_project_id=(#{subquery}) WHERE gl_project_id IS NULL")
......
class AddIndexToCiTables < ActiveRecord::Migration
def up
def change
add_index :ci_builds, :gl_project_id
add_index :ci_runner_projects, :gl_project_id
add_index :ci_triggers, :gl_project_id
......
class DropNullForCiTables < ActiveRecord::Migration
def up
def change
remove_index :ci_variables, :project_id
remove_index :ci_runner_projects, :project_id
change_column_null :ci_triggers, :project_id, true
......
......@@ -837,6 +837,7 @@ ActiveRecord::Schema.define(version: 20151210125932) do
t.integer "consumed_timestep"
t.integer "layout", default: 0
t.boolean "hide_project_limit", default: false
t.string "unlock_token"
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
......
......@@ -118,6 +118,16 @@ Parameters:
"path": "brightbox",
"updated_at": "2013-09-30T13:46:02Z"
},
"permissions": {
"project_access": {
"access_level": 10,
"notification_level": 3
},
"group_access": {
"access_level": 50,
"notification_level": 3
}
},
"archived": false,
"avatar_url": null
}
......
......@@ -24,6 +24,7 @@
### Examples
+ [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
+ [Test and deploy Ruby applications to Heroku](examples/test-and-deploy-ruby-application-to-heroku.md)
+ [Test and deploy Python applications to Heroku](examples/test-and-deploy-python-application-to-heroku.md)
+ [Test Clojure applications](examples/test-clojure-application.md)
......
......@@ -4,10 +4,12 @@ GitLab integrates with multiple third-party services to allow external issue tra
See the documentation below for details on how to configure these services.
- [Jira](jira.md) Integrate with the JIRA issue tracker
- [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc.
- [LDAP](ldap.md) Set up sign in via LDAP
- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab, and Google via OAuth.
- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [CAS](cas.md) Configure GitLab to sign in using CAS
- [Slack](slack.md) Integrate with the Slack chat service
- [OAuth2 provider](oauth_provider.md) OAuth2 application creation
- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
......
# CAS OmniAuth Provider
To enable the CAS OmniAuth provider you must register your application with your CAS instance. This requires the service URL GitLab will supply to CAS. It should be something like: `https://gitlab.example.com:443/users/auth/cas3/callback?url`. By default handling for SLO is enabled, you only need to configure CAS for backchannel logout.
1. On your GitLab server, open the configuration file.
For omnibus package:
```sh
sudo editor /etc/gitlab/gitlab.rb
```
For installations from source:
```sh
cd /home/git/gitlab
sudo -u git -H editor config/gitlab.yml
```
1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
1. Add the provider configuration:
For omnibus package:
```ruby
gitlab_rails['omniauth_providers'] = [
{
name: "cas3",
label: "cas",
args: {
url: 'CAS_SERVER',
login_url: '/CAS_PATH/login',
service_validate_url: '/CAS_PATH/p3/serviceValidate',
logout_url: '/CAS_PATH/logout'} }
}
}
]
```
For installations from source:
```
- { name: 'cas3',
label: 'cas',
args: {
url: 'CAS_SERVER',
login_url: '/CAS_PATH/login',
service_validate_url: '/CAS_PATH/p3/serviceValidate',
logout_url: '/CAS_PATH/logout'} }
```
1. Change 'CAS_PATH' to the root of your CAS instance (ie. `cas`).
1. If your CAS instance does not use default TGC lifetimes, update the `cas3.session_duration` to at least the current TGC maximum lifetime. To explicitly disable SLO, regardless of CAS settings, set this to 0.
1. Save the configuration file.
1. Restart GitLab for the changes to take effect.
On the sign in page there should now be a CAS tab in the sign in form.
# GitLab Jira integration
GitLab can be configured to interact with Jira.
Configuration happens via username and password.
Connecting to a Jira server via CAS is not possible.
Each project can be configured to connect to a different Jira instance, configuration is explained [here](#configuration).
If you have one Jira instance you can pre-fill the settings page with a default template. To configure the template [see external issue tracker document](external-issue-tracker.md#service-template)).
Once the project is connected to Jira, you can reference and close the issues in Jira directly from GitLab.
## Table of Contents
* [Referencing Jira Issues from GitLab](#referencing-jira-issues)
* [Closing Jira Issues from GitLab](#closing-jira-issues)
* [Configuration](#configuration)
### Referencing Jira Issues
When GitLab project has Jira issue tracker configured and enabled, mentioning Jira issue in GitLab will automatically add a comment in Jira issue with the link back to GitLab. This means that in comments in merge requests and commits referencing an issue, eg. `PROJECT-7`, will add a comment in Jira issue in the format:
```
USER mentioned this issue in LINK_TO_THE_MENTION
```
* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab.
* `LINK_TO_THE_MENTION` Link to the origin of mention with a name of the entity where Jira issue was mentioned.
Can be commit or merge request.
![example of mentioning or closing the Jira issue](jira_issue_reference.png)
### Closing Jira Issues
Jira issues can be closed directly from GitLab by using trigger words, eg. `Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and merge requests.
When a commit which contains the trigger word in the commit message is pushed, GitLab will add a comment in the mentioned Jira issue.
For example, for project named PROJECT in Jira, we implemented a new feature and created a merge request in GitLab.
This feature was requested in Jira issue PROJECT-7. Merge request in GitLab contains the improvement and in merge request description we say that this merge request `Closes PROJECT-7` issue.
Once this merge request is merged, Jira issue will be automatically closed with a link to the commit that resolved the issue.
![A Git commit that causes the Jira issue to be closed](merge_request_close_jira.png)
![The GitLab integration user leaves a comment on Jira](jira_service_close_issue.png)
## Configuration
### Configuring JIRA
We need to create a user in JIRA which will have access to all projects that need to integrate with GitLab.
Login to your JIRA instance as admin and under Administration go to User Management and create a new user.
As an example, we'll create a user named `gitlab` and add it to `jira-developers` group.
**It is important that the user `gitlab` has write-access to projects in JIRA**
### Configuring GitLab
### GitLab 7.8 EE and up with JIRA v6.x
To enable JIRA integration in a project, navigate to the project Settings page and go to Services. Here you will find JIRA.
Fill in the required details on the page:
![Jira service page](jira_service_page.png)
* `description` A name for the issue tracker (to differentiate between instances, for instance).
* `project url` The URL to the JIRA project which is being linked to this GitLab project.
* `issues url` The URL to the JIRA project issues overview for the project that is linked to this GitLab project.
* `new issue url` This is the URL to create a new issue in JIRA for the project linked to this GitLab project.
* `api url` The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`, i.e. `https://jira.example.com/rest/api/2`.
* `username` The username of the user created in [configuring JIRA step](#configuring-jira).
* `password` The password of the user created in [configuring JIRA step](#configuring-jira).
* `Jira issue transition` This is the id of a transition that moves issues to a closed state. You can find this number under [JIRA workflow administration, see screenshot](jira_workflow_screenshot.png). By default, this id is `2`. (In the example image, this is `2` as well)
After saving the configuration, your GitLab project will be able to interact with the linked JIRA project.
### GitLab 6.x-7.7 with JIRA v6.x
**Note: GitLab 7.8 and up contain various integration improvements. We strongly recommend upgrading.**
In `gitlab.yml` enable [JIRA issue tracker section by uncommenting the lines](https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115).
This will make sure that all issues within GitLab are pointing to the JIRA issue tracker.
We can also enable JIRA service that will allow us to interact with JIRA issues.
For example, we can close issues in JIRA by a commit in GitLab.
Go to project settings page and fill in the project name for the JIRA project:
![Set the JIRA project name in GitLab to 'NEW'](jira_project_name.png)
Next, go to the services page and find JIRA.
![Jira services page](jira_service.png)
1. Tick the active check box to enable the service.
1. Supply the url to JIRA server, for example http://jira.sample
1. Supply the username of a user we created under `Configuring JIRA` section, for example `gitlab`
1. Supply the password of the user
1. Optional: supply the JIRA api version, default is version
1. Optional: supply the JIRA issue transition ID (issue transition to closed). This is dependant on JIRA settings, default is 2
1. Save
Now we should be able to interact with JIRA issues.
......@@ -13,6 +13,11 @@ Feature: Award Emoji
Then I have award added
And I can remove it by clicking to icon
@javascript
Scenario: I can see the list of emoji categories
Given I click to emoji-picker
Then I can see the activity and food categories
@javascript
Scenario: I add award emoji using regular comment
Given I leave comment with a single emoji
......
......@@ -55,6 +55,12 @@ Feature: Project Services
And I fill email on push settings
Then I should see email on push service settings saved
Scenario: Activate JIRA service
When I visit project "Shop" services page
And I click jira service link
And I fill jira settings
Then I should see jira service settings saved
Scenario: Activate Irker (IRC Gateway) service
When I visit project "Shop" services page
And I click Irker service link
......
......@@ -15,8 +15,8 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end
step 'I click to emoji in the picker' do
page.within '.awards-menu' do
page.first('img').click
page.within '.emoji-menu' do
page.first('.emoji-icon').click
end
end
......@@ -27,6 +27,13 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end
end
step 'I can see the activity and food categories' do
page.within '.emoji-menu' do
expect(page).to_not have_selector 'Activity'
expect(page).to_not have_selector 'Food'
end
end
step 'I have award added' do
page.within '.awards' do
expect(page).to have_selector '.award'
......
......@@ -173,6 +173,24 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
expect(find_field('Sound').find('option[selected]').value).to eq 'bike'
end
step 'I click jira service link' do
click_link 'JIRA'
end
step 'I fill jira settings' do
fill_in 'Project url', with: 'http://jira.example'
fill_in 'Username', with: 'gitlab'
fill_in 'Password', with: 'gitlab'
fill_in 'Api url', with: 'http://jira.example/rest/api/2'
click_button 'Save'
end
step 'I should see jira service settings saved' do
expect(find_field('Project url').value).to eq 'http://jira.example'
expect(find_field('Username').value).to eq 'gitlab'
expect(find_field('Api url').value).to eq 'http://jira.example/rest/api/2'
end
step 'I click Atlassian Bamboo CI service link' do
click_link 'Atlassian Bamboo CI'
end
......
......@@ -25,7 +25,7 @@ module API
@projects = current_user.authorized_projects
@projects = filter_projects(@projects)
@projects = paginate @projects
present @projects, with: Entities::Project
present @projects, with: Entities::ProjectWithAccess, user: current_user
end
# Get an owned projects list for authenticated user
......@@ -36,7 +36,7 @@ module API
@projects = current_user.owned_projects
@projects = filter_projects(@projects)
@projects = paginate @projects
present @projects, with: Entities::Project
present @projects, with: Entities::ProjectWithAccess, user: current_user
end
# Gets starred project for the authenticated user
......@@ -59,7 +59,7 @@ module API
@projects = Project.all
@projects = filter_projects(@projects)
@projects = paginate @projects
present @projects, with: Entities::Project
present @projects, with: Entities::ProjectWithAccess, user: current_user
end
# Get a single project
......
class AwardEmoji
EMOJI_LIST = [
"+1", "-1", "100", "blush", "heart", "smile", "rage",
"beers", "disappointed", "ok_hand",
"helicopter", "shit", "airplane", "alarm_clock",
"ambulance", "anguished", "two_hearts", "wink"
]
ALIASES = {
pout: "rage",
satisfied: "laughing",
......@@ -37,11 +30,41 @@ class AwardEmoji
squirrel: "shipit"
}.with_indifferent_access
def self.path_to_emoji_image(name)
"emoji/#{Emoji.emoji_filename(name)}.png"
end
CATEGORIES = {
other: "Other",
objects: "Objects",
places: "Places",
travel_places: "Travel",
emoticons: "Emoticons",
objects_symbols: "Symbols",
nature: "Nature",
celebration: "Celebration",
people: "People",
activity: "Activity",
flags: "Flags",
food_drink: "Food"
}.with_indifferent_access
def self.normilize_emoji_name(name)
ALIASES[name] || name
end
def self.emoji_by_category
unless @emoji_by_category
@emoji_by_category = {}
emojis_added = []
Emoji.emojis.each do |emoji_name, data|
next if emojis_added.include?(data["name"])
emojis_added << data["name"]
@emoji_by_category[data["category"]] ||= []
@emoji_by_category[data["category"]] << data
end
@emoji_by_category = @emoji_by_category.sort.to_h
end
@emoji_by_category
end
end
......@@ -23,6 +23,18 @@ module Banzai
end
end
def self.referenced_by(node)
project = Project.find(node.attr("data-project")) rescue nil
return unless project
id = node.attr("data-external-issue")
external_issue = ExternalIssue.new(id, project)
return unless external_issue
{ external_issue: external_issue }
end
def call
# Early return if the project isn't using an external tracker
return doc if project.nil? || project.default_issues_tracker?
......@@ -46,12 +58,14 @@ module Banzai
def issue_link_filter(text, link_text: nil)
project = context[:project]
self.class.references_in(text) do |match, issue|
url = url_for_issue(issue, project, only_path: context[:only_path])
self.class.references_in(text) do |match, id|
ExternalIssue.new(id, project)
url = url_for_issue(id, project, only_path: context[:only_path])
title = escape_once("Issue in #{project.external_issue_tracker.title}")
klass = reference_class(:issue)
data = data_attribute(project: project.id)
data = data_attribute(project: project.id, external_issue: id)
text = link_text || match
......
......@@ -19,7 +19,7 @@ module Ci
end
def runner_registration_token_valid?
params[:token] == current_application_settings.ensure_runners_registration_token
params[:token] == current_application_settings.runners_registration_token
end
def update_runner_last_contact
......
......@@ -20,6 +20,10 @@ module Gitlab
def blank_ref?(ref)
ref == BLANK_SHA
end
def version
Gitlab::VersionInfo.parse(Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --version)).first)
end
end
end
end
module Gitlab
module OAuth
module Session
def self.create(provider, ticket)
Rails.cache.write("gitlab:#{provider}:#{ticket}", ticket, expires_in: Gitlab.config.omniauth.cas3.session_duration)
end
def self.destroy(provider, ticket)
Rails.cache.delete("gitlab:#{provider}:#{ticket}")
end
def self.valid?(provider, ticket)
Rails.cache.read("gitlab:#{provider}:#{ticket}").present?
end
end
end
end
......@@ -18,10 +18,20 @@ module Gitlab
super(text, context.merge(project: project))
end
%i(user label issue merge_request snippet commit commit_range).each do |type|
%i(user label merge_request snippet commit commit_range).each do |type|
define_method("#{type}s") do
@references[type] ||= references(type, project: project, current_user: current_user)
end
end
def issues
options = { project: project, current_user: current_user }
if project && project.jira_tracker?
@references[:external_issue] ||= references(:external_issue, options)
else
@references[:issue] ||= references(:issue, options)
end
end
end
end
......@@ -63,7 +63,7 @@ describe "Admin Runners" do
end
describe 'runners registration token' do
let!(:token) { current_application_settings.ensure_runners_registration_token }
let!(:token) { current_application_settings.runners_registration_token }
before { visit admin_runners_path }
it 'has a registration token' do
......
require 'spec_helper'
describe 'CI Lint' do
before do
login_as :user
end
describe 'YAML parsing' do
before do
visit ci_lint_path
fill_in 'content', with: yaml_content
click_on 'Validate'
end
context 'YAML is correct' do
let(:yaml_content) do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
end
it 'Yaml parsing' do
within "table" do
expect(page).to have_content('Job - rspec')
expect(page).to have_content('Job - spinach')
expect(page).to have_content('Deploy Job - staging')
expect(page).to have_content('Deploy Job - production')
end
end
end
context 'YAML is incorrect' do
let(:yaml_content) { '' }
it 'displays information about an error' do
expect(page).to have_content('Status: syntax is incorrect')
expect(page).to have_content('Error: Please provide content of .gitlab-ci.yml')
end
end
end
end
require 'spec_helper'
describe "Lint" do
before do
login_as :user
end
it "Yaml parsing", js: true do
content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
visit ci_lint_path
fill_in "content", with: content
click_on "Validate"
within "table" do
expect(page).to have_content("Job - rspec")
expect(page).to have_content("Job - spinach")
expect(page).to have_content("Deploy Job - staging")
expect(page).to have_content("Deploy Job - production")
end
end
it "Yaml parsing with error", js: true do
visit ci_lint_path
fill_in "content", with: ""
click_on "Validate"
expect(page).to have_content("Status: syntax is incorrect")
expect(page).to have_content("Error: Please provide content of .gitlab-ci.yml")
end
end
......@@ -70,6 +70,20 @@ feature 'Project', feature: true do
end
end
describe 'leave project link' do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
before do
login_with(user)
project.team.add_user(user, Gitlab::Access::MASTER)
visit namespace_project_path(project.namespace, project)
end
it { expect(page).to have_content('You have Master access to this project.') }
it { expect(page).to have_link('Leave this project') }
end
def remove_with_confirm(button_text, confirm_with)
click_button button_text
fill_in 'confirm_name_input', with: confirm_with
......
......@@ -127,18 +127,6 @@ describe IssuesHelper do
it { is_expected.to eq("!1, !2, or !3") }
end
describe "#url_to_emoji" do
it "returns url" do
expect(url_to_emoji("smile")).to include("emoji/1F604.png")
end
end
describe "#emoji_list" do
it "returns url" do
expect(emoji_list).to be_kind_of(Array)
end
end
describe "#note_active_class" do
before do
@note = create :note
......
require 'spec_helper'
describe MergeRequestsHelper do
describe "#issues_sentence" do
describe 'ci_build_details_path' do
let(:project) { create :project }
let(:merge_request) { MergeRequest.new }
let(:ci_service) { CiService.new }
let(:last_commit) { Ci::Commit.new({}) }
before do
allow(merge_request).to receive(:source_project).and_return(project)
allow(merge_request).to receive(:last_commit).and_return(last_commit)
allow(project).to receive(:ci_service).and_return(ci_service)
allow(last_commit).to receive(:sha).and_return('12d65c')
end
it 'does not include api credentials in a link' do
allow(ci_service).
to receive(:build_page).and_return("http://secretuser:secretpass@jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c")
expect(helper.ci_build_details_path(merge_request)).to_not match("secret")
end
end
describe '#issues_sentence' do
subject { issues_sentence(issues) }
let(:issues) do
[build(:issue, iid: 1), build(:issue, iid: 2), build(:issue, iid: 3)]
end
it { is_expected.to eq('#1, #2, and #3') }
context 'for JIRA issues' do
let(:project) { create(:project) }
let(:issues) do
[
JiraIssue.new('JIRA-123', project),
JiraIssue.new('JIRA-456', project),
JiraIssue.new('FOOBAR-7890', project)
]
end
it { is_expected.to eq('FOOBAR-7890, JIRA-123, and JIRA-456') }
end
end
describe "#format_mr_branch_names" do
describe "within the same project" do
describe '#format_mr_branch_names' do
describe 'within the same project' do
let(:merge_request) { create(:merge_request) }
subject { format_mr_branch_names(merge_request) }
it { is_expected.to eq([merge_request.source_branch, merge_request.target_branch]) }
end
describe "within different projects" do
describe 'within different projects' do
let(:project) { create(:project) }
let(:fork_project) { create(:project, forked_from_project: project) }
let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: project) }
......
......@@ -53,6 +53,16 @@ describe ProjectsHelper do
end
end
describe 'user_max_access_in_project' do
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
project.team.add_user(user, Gitlab::Access::MASTER)
end
it { expect(helper.user_max_access_in_project(user.id, project)).to eq('Master') }
end
describe "readme_cache_key" do
let(:project) { create(:project) }
......
......@@ -97,6 +97,16 @@ describe Gitlab::ReferenceExtractor, lib: true do
expect(extracted.first.commit_to).to eq commit
end
context 'with an external issue tracker' do
let(:project) { create(:jira_project) }
subject { described_class.new(project, project.creator) }
it 'returns JIRA issues for a JIRA-integrated project' do
subject.analyze('JIRA-123 and FOOBAR-4567')
expect(subject.issues).to eq [JiraIssue.new('JIRA-123', project), JiraIssue.new('FOOBAR-4567', project)]
end
end
context 'with a project with an underscore' do
let(:other_project) { create(:project, path: 'test_project') }
let(:issue) { create(:issue, project: other_project) }
......
......@@ -189,6 +189,12 @@ describe Ci::Build, models: true do
it { is_expected.to eq(98.29) }
end
context 'using a regex capture' do
subject { build.extract_coverage('TOTAL 9926 3489 65%', 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)') }
it { is_expected.to eq(65) }
end
end
describe :variables do
......@@ -390,4 +396,68 @@ describe Ci::Build, models: true do
it { is_expected.to include('gitlab-ci-token') }
it { is_expected.to include(project.web_url[7..-1]) }
end
def create_mr(build, commit, factory: :merge_request, created_at: Time.now)
FactoryGirl.create(factory,
source_project_id: commit.gl_project_id,
target_project_id: commit.gl_project_id,
source_branch: build.ref,
created_at: created_at)
end
describe :merge_request do
context 'when a MR has a reference to the commit' do
before do
@merge_request = create_mr(build, commit, factory: :merge_request)
commits = [double(id: commit.sha)]
allow(@merge_request).to receive(:commits).and_return(commits)
allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
end
it 'returns the single associated MR' do
expect(build.merge_request.id).to eq(@merge_request.id)
end
end
context 'when there is not a MR referencing the commit' do
it 'returns nil' do
expect(build.merge_request).to be_nil
end
end
context 'when more than one MR have a reference to the commit' do
before do
@merge_request = create_mr(build, commit, factory: :merge_request)
@merge_request.close!
@merge_request2 = create_mr(build, commit, factory: :merge_request)
commits = [double(id: commit.sha)]
allow(@merge_request).to receive(:commits).and_return(commits)
allow(@merge_request2).to receive(:commits).and_return(commits)
allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request, @merge_request2])
end
it 'returns the first MR' do
expect(build.merge_request.id).to eq(@merge_request.id)
end
end
context 'when a Build is created after the MR' do
before do
@merge_request = create_mr(build, commit, factory: :merge_request_with_diffs)
commit2 = FactoryGirl.create :ci_commit, project: project
@build2 = FactoryGirl.create :ci_build, commit: commit2
commits = [double(id: commit.sha), double(id: commit2.sha)]
allow(@merge_request).to receive(:commits).and_return(commits)
allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
end
it 'returns the current MR' do
expect(@build2.merge_request.id).to eq(@merge_request.id)
end
end
end
end
require 'spec_helper'
describe Mentionable do
include Mentionable
describe :references do
let(:project) { create(:project) }
it 'excludes JIRA references' do
allow(project).to receive_messages(jira_tracker?: true)
expect(referenced_mentionables(project, 'JIRA-123')).to be_empty
end
end
end
describe Issue, "Mentionable" do
describe '#mentioned_users' do
let!(:user) { create(:user, username: 'stranger') }
......
......@@ -2,7 +2,8 @@ require 'spec_helper'
shared_examples 'TokenAuthenticatable' do
describe 'dynamically defined methods' do
it { expect(described_class).to be_private_method_defined(:generate_token_for) }
it { expect(described_class).to be_private_method_defined(:generate_token) }
it { expect(described_class).to be_private_method_defined(:write_new_token) }
it { expect(described_class).to respond_to("find_by_#{token_field}") }
it { is_expected.to respond_to("ensure_#{token_field}") }
it { is_expected.to respond_to("reset_#{token_field}!") }
......@@ -24,11 +25,11 @@ describe ApplicationSetting, 'TokenAuthenticatable' do
it_behaves_like 'TokenAuthenticatable'
describe 'generating new token' do
subject { described_class.new }
let(:token) { subject.send(token_field) }
context 'token is not generated yet' do
it { expect(token).to be nil }
describe 'token field accessor' do
subject { described_class.new.send(token_field) }
it { is_expected.to_not be_blank }
end
describe 'ensured token' do
subject { described_class.new.send("ensure_#{token_field}") }
......@@ -36,11 +37,21 @@ describe ApplicationSetting, 'TokenAuthenticatable' do
it { is_expected.to be_a String }
it { is_expected.to_not be_blank }
end
describe 'ensured! token' do
subject { described_class.new.send("ensure_#{token_field}!") }
it 'should persist new token' do
expect(subject).to eq described_class.current[token_field]
end
end
end
context 'token is generated' do
before { subject.send("reset_#{token_field}!") }
it { expect(token).to be_a String }
it 'persists a new token 'do
expect(subject.send(:read_attribute, token_field)).to be_a String
end
end
end
......
require 'spec_helper'
describe JiraIssue do
let(:project) { create(:project) }
subject { JiraIssue.new('JIRA-123', project) }
describe 'id' do
subject { super().id }
it { is_expected.to eq('JIRA-123') }
end
describe 'iid' do
subject { super().iid }
it { is_expected.to eq('JIRA-123') }
end
describe 'to_s' do
subject { super().to_s }
it { is_expected.to eq('JIRA-123') }
end
describe :== do
specify { expect(subject).to eq(JiraIssue.new('JIRA-123', project)) }
specify { expect(subject).not_to eq(JiraIssue.new('JIRA-124', project)) }
it 'only compares with JiraIssues' do
expect(subject).not_to eq('JIRA-123')
end
end
end
......@@ -164,6 +164,17 @@ describe MergeRequest, models: true do
expect(subject.closes_issues).to include(issue2)
end
context 'for a project with JIRA integration' do
let(:issue0) { JiraIssue.new('JIRA-123', subject.project) }
let(:issue1) { JiraIssue.new('FOOBAR-4567', subject.project) }
it 'returns sorted JiraIssues' do
allow(subject.project).to receive_messages(default_branch: subject.target_branch)
expect(subject.closes_issues).to eq([issue0, issue1])
end
end
end
describe "#work_in_progress?" do
......
......@@ -26,6 +26,113 @@ describe JiraService, models: true do
it { is_expected.to have_one :service_hook }
end
describe "Execute" do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request) }
before do
@jira_service = JiraService.new
allow(@jira_service).to receive_messages(
project_id: project.id,
project: project,
service_hook: true,
project_url: 'http://jira.example.com',
username: 'gitlab_jira_username',
password: 'gitlab_jira_password'
)
@jira_service.save # will build API URL, as api_url was not specified above
@sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
# https://github.com/bblimke/webmock#request-with-basic-authentication
@api_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions'
@comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment'
WebMock.stub_request(:post, @api_url)
WebMock.stub_request(:post, @comment_url)
end
it "should call JIRA API" do
@jira_service.execute(merge_request, JiraIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @comment_url).with(
body: /Issue solved with/
).once
end
it "calls the api with jira_issue_transition_id" do
@jira_service.jira_issue_transition_id = 'this-is-a-custom-id'
@jira_service.execute(merge_request, JiraIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @api_url).with(
body: /this-is-a-custom-id/
).once
end
end
describe "Stored password invalidation" do
let(:project) { create(:project) }
context "when a password was previously set" do
before do
@jira_service = JiraService.create(
project: create(:project),
properties: {
api_url: 'http://jira.example.com/rest/api/2',
username: 'mic',
password: "password"
}
)
end
it "reset password if url changed" do
@jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
@jira_service.save
expect(@jira_service.password).to be_nil
end
it "does not reset password if username changed" do
@jira_service.username = "some_name"
@jira_service.save
expect(@jira_service.password).to eq("password")
end
it "does not reset password if new url is set together with password, even if it's the same password" do
@jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
@jira_service.password = 'password'
@jira_service.save
expect(@jira_service.password).to eq("password")
expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2")
end
it "should reset password if url changed, even if setter called multiple times" do
@jira_service.api_url = 'http://jira1.example.com/rest/api/2'
@jira_service.api_url = 'http://jira1.example.com/rest/api/2'
@jira_service.save
expect(@jira_service.password).to be_nil
end
end
context "when no password was previously set" do
before do
@jira_service = JiraService.create(
project: create(:project),
properties: {
api_url: 'http://jira.example.com/rest/api/2',
username: 'mic'
}
)
end
it "saves password if new url is set together with password" do
@jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
@jira_service.password = 'password'
@jira_service.save
expect(@jira_service.password).to eq("password")
expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2")
end
end
end
describe "Validations" do
context "active" do
before do
......@@ -78,7 +185,8 @@ describe JiraService, models: true do
context 'when gitlab.yml was initialized' do
before do
settings = { "jira" => {
settings = {
"jira" => {
"title" => "Jira",
"project_url" => "http://jira.sample/projects/project_a",
"issues_url" => "http://jira.sample/issues/:id",
......
......@@ -26,6 +26,7 @@
# bio :string(255)
# failed_attempts :integer default(0)
# locked_at :datetime
# unlock_token :string(255)
# username :string(255)
# can_create_group :boolean default(TRUE), not null
# can_create_team :boolean default(TRUE), not null
......
......@@ -131,6 +131,7 @@ describe API::API, api: true do
expect(json_response).to satisfy do |response|
response.one? do |entry|
entry.has_key?('permissions') &&
entry['name'] == project.name &&
entry['owner']['username'] == user.username
end
......@@ -382,6 +383,18 @@ describe API::API, api: true do
end
describe 'permissions' do
context 'all projects' do
it 'Contains permission information' do
project.team << [user, :master]
get api("/projects", user)
expect(response.status).to eq(200)
expect(json_response.first['permissions']['project_access']['access_level']).
to eq(Gitlab::Access::MASTER)
expect(json_response.first['permissions']['group_access']).to be_nil
end
end
context 'personal project' do
it 'Sets project access and returns 200' do
project.team << [user, :master]
......
......@@ -8,7 +8,6 @@ describe Ci::API::API do
before do
stub_gitlab_calls
stub_application_setting(ensure_runners_registration_token: registration_token)
stub_application_setting(runners_registration_token: registration_token)
end
......
......@@ -265,6 +265,75 @@ describe GitPushService, services: true do
expect(Issue.find(issue.id)).to be_opened
end
end
# EE-only tests
context "for jira issue tracker" do
include JiraServiceHelper
let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? }
before do
jira_service_settings
WebMock.stub_request(:post, jira_api_transition_url)
WebMock.stub_request(:post, jira_api_comment_url)
WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
WebMock.stub_request(:get, jira_api_test_url)
allow(closing_commit).to receive_messages({
issue_closing_regex: Regexp.new(Gitlab.config.gitlab.issue_closing_pattern),
safe_message: message,
author_name: commit_author.name,
author_email: commit_author.email
})
allow(project.repository).to receive_messages(commits_between: [closing_commit])
end
after do
jira_tracker.destroy!
end
context "mentioning an issue" do
let(:message) { "this is some work.\n\nrelated to JIRA-1" }
it "should initiate one api call to jira server to mention the issue" do
service.execute(project, user, @oldrev, @newrev, @ref)
expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
body: /mentioned this issue in/
).once
end
end
context "closing an issue" do
let(:message) { "this is some work.\n\ncloses JIRA-1" }
it "should initiate one api call to jira server to close the issue" do
transition_body = {
transition: {
id: '2'
}
}.to_json
service.execute(project, user, @oldrev, @newrev, @ref)
expect(WebMock).to have_requested(:post, jira_api_transition_url).with(
body: transition_body
).once
end
it "should initiate one api call to jira server to comment on the issue" do
comment_body = {
body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]."
}.to_json
service.execute(project, user, @oldrev, @newrev, @ref)
expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
body: comment_body
).once
end
end
end
end
describe "empty project" do
......
......@@ -425,4 +425,65 @@ describe SystemNoteService, services: true do
end
end
end
include JiraServiceHelper
describe 'JIRA integration' do
let(:project) { create(:project) }
let(:author) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) }
let(:jira_issue) { JiraIssue.new("JIRA-1", project)}
let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? }
let(:commit) { project.commit }
context 'in JIRA issue tracker' do
before do
jira_service_settings
WebMock.stub_request(:post, jira_api_comment_url)
end
after do
jira_tracker.destroy!
end
describe "new reference" do
before do
WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
end
subject { described_class.cross_reference(jira_issue, commit, author) }
it { is_expected.to eq(jira_status_message) }
end
describe "existing reference" do
before do
message = "[#{author.name}|http://localhost/u/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]."
WebMock.stub_request(:get, jira_api_comment_url).to_return(body: "{\"comments\":[{\"body\":\"#{message}\"}]}")
end
subject { described_class.cross_reference(jira_issue, commit, author) }
it { is_expected.not_to eq(jira_status_message) }
end
end
context 'issue from an issue' do
context 'in JIRA issue tracker' do
before do
jira_service_settings
WebMock.stub_request(:post, jira_api_comment_url)
WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
end
after do
jira_tracker.destroy!
end
subject { described_class.cross_reference(jira_issue, issue, author) }
it { is_expected.to eq(jira_status_message) }
end
end
end
end
module JiraServiceHelper
def jira_service_settings
properties = {
"title"=>"JIRA tracker",
"project_url"=>"http://jira.example/issues/?jql=project=A",
"issues_url"=>"http://jira.example/browse/JIRA-1",
"new_issue_url"=>"http://jira.example/secure/CreateIssue.jspa",
"api_url"=>"http://jira.example/rest/api/2"
}
jira_tracker.update_attributes(properties: properties, active: true)
end
def jira_status_message
"JiraService SUCCESS 200: Successfully posted to #{jira_api_comment_url}."
end
def jira_issue_comments
"{\"startAt\":0,\"maxResults\":11,\"total\":11,
\"comments\":[{\"self\":\"http://0.0.0.0:4567/rest/api/2/issue/10002/comment/10609\",
\"id\":\"10609\",\"author\":{\"self\":\"http://0.0.0.0:4567/rest/api/2/user?username=gitlab\",
\"name\":\"gitlab\",\"emailAddress\":\"gitlab@example.com\",
\"avatarUrls\":{\"16x16\":\"http://0.0.0.0:4567/secure/useravatar?size=xsmall&avatarId=10122\",
\"24x24\":\"http://0.0.0.0:4567/secure/useravatar?size=small&avatarId=10122\",
\"32x32\":\"http://0.0.0.0:4567/secure/useravatar?size=medium&avatarId=10122\",
\"48x48\":\"http://0.0.0.0:4567/secure/useravatar?avatarId=10122\"},
\"displayName\":\"GitLab\",\"active\":true},
\"body\":\"[Administrator|http://localhost:3000/u/root] mentioned JIRA-1 in Merge request of [gitlab-org/gitlab-test|http://localhost:3000/gitlab-org/gitlab-test/merge_requests/2].\",
\"updateAuthor\":{\"self\":\"http://0.0.0.0:4567/rest/api/2/user?username=gitlab\",\"name\":\"gitlab\",\"emailAddress\":\"gitlab@example.com\",
\"avatarUrls\":{\"16x16\":\"http://0.0.0.0:4567/secure/useravatar?size=xsmall&avatarId=10122\",
\"24x24\":\"http://0.0.0.0:4567/secure/useravatar?size=small&avatarId=10122\",
\"32x32\":\"http://0.0.0.0:4567/secure/useravatar?size=medium&avatarId=10122\",
\"48x48\":\"http://0.0.0.0:4567/secure/useravatar?avatarId=10122\"},\"displayName\":\"GitLab\",\"active\":true},
\"created\":\"2015-02-12T22:47:07.826+0100\",
\"updated\":\"2015-02-12T22:47:07.826+0100\"},
{\"self\":\"http://0.0.0.0:4567/rest/api/2/issue/10002/comment/10700\",
\"id\":\"10700\",\"author\":{\"self\":\"http://0.0.0.0:4567/rest/api/2/user?username=gitlab\",
\"name\":\"gitlab\",\"emailAddress\":\"gitlab@example.com\",
\"avatarUrls\":{\"16x16\":\"http://0.0.0.0:4567/secure/useravatar?size=xsmall&avatarId=10122\",
\"24x24\":\"http://0.0.0.0:4567/secure/useravatar?size=small&avatarId=10122\",
\"32x32\":\"http://0.0.0.0:4567/secure/useravatar?size=medium&avatarId=10122\",
\"48x48\":\"http://0.0.0.0:4567/secure/useravatar?avatarId=10122\"},\"displayName\":\"GitLab\",\"active\":true},
\"body\":\"[Administrator|http://localhost:3000/u/root] mentioned this issue in [a commit of h5bp/html5-boilerplate|http://localhost:3000/h5bp/html5-boilerplate/commit/2439f77897122fbeee3bfd9bb692d3608848433e].\",
\"updateAuthor\":{\"self\":\"http://0.0.0.0:4567/rest/api/2/user?username=gitlab\",\"name\":\"gitlab\",\"emailAddress\":\"gitlab@example.com\",
\"avatarUrls\":{\"16x16\":\"http://0.0.0.0:4567/secure/useravatar?size=xsmall&avatarId=10122\",
\"24x24\":\"http://0.0.0.0:4567/secure/useravatar?size=small&avatarId=10122\",
\"32x32\":\"http://0.0.0.0:4567/secure/useravatar?size=medium&avatarId=10122\",
\"48x48\":\"http://0.0.0.0:4567/secure/useravatar?avatarId=10122\"},\"displayName\":\"GitLab\",\"active\":true},
\"created\":\"2015-04-01T03:45:55.667+0200\",
\"updated\":\"2015-04-01T03:45:55.667+0200\"
}
]}"
end
def jira_api_comment_url
'http://jira.example/rest/api/2/issue/JIRA-1/comment'
end
def jira_api_transition_url
'http://jira.example/rest/api/2/issue/JIRA-1/transitions'
end
def jira_api_test_url
'http://jira.example/rest/api/2/myself'
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment