Commit b6add255 authored by Valery Sizov's avatar Valery Sizov

Merge branch 'ce_upstream' into 'master'

CE upstream

Unmerged paths:
  (use "git add <file>..." to mark resolution)

	both modified:   app/assets/stylesheets/pages/projects.scss
	both modified:   app/views/shared/_clone_panel.html.haml

@jschatz1 Please resolve these conflicts. 

See merge request !99
parents 8ad2f98b 7b24ea5c
......@@ -4,12 +4,28 @@ v 8.3.0 (unreleased)
- Merge when build succeeds (Zeger-Jan van de Weg)
v 8.4.0 (unreleased)
- Implement new UI for group page
- Implement search inside emoji picker
- Add API support for looking up a user by username (Stan Hu)
- Add project permissions to all project API endpoints (Stan Hu)
- Expose Git's version in the admin area
- Add "Frequently used" category to emoji picker
- Add CAS support (tduehr)
- Add link to merge request on build detail page.
- Revert back upvote and downvote button to the issue and MR pages
v 8.3.2 (unreleased)
- Enable "Add key" button when user fills in a proper key
v 8.3.1
- Fix Error 500 when global milestones have slashes (Stan Hu)
- Fix Error 500 when doing a search in dashboard before visiting any project (Stan Hu)
- Fix LDAP identity and user retrieval when special characters are used
- Move Sidekiq-cron configuration to gitlab.yml
- Enable forcing Two-Factor authentication sitewide, with optional grace period
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)
......@@ -70,7 +86,6 @@ v 8.3.0
- 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)
......
......@@ -177,7 +177,7 @@ gem 'd3_rails', '~> 3.5.5'
gem "cal-heatmap-rails", "~> 0.0.1"
# underscore-rails
gem "underscore-rails", "~> 1.4.4"
gem "underscore-rails", "~> 1.8.0"
# Sanitize user input
gem "sanitize", '~> 2.0'
......@@ -195,7 +195,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'
......@@ -207,9 +207,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'
......
......@@ -377,15 +377,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)
......@@ -652,12 +653,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)
......@@ -772,7 +774,7 @@ GEM
uglifier (2.7.2)
execjs (>= 0.3.0)
json (>= 1.8.0)
underscore-rails (1.4.4)
underscore-rails (1.8.3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.1)
......@@ -886,10 +888,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)
......@@ -943,7 +945,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)
......@@ -972,7 +974,7 @@ DEPENDENCIES
tinder (~> 1.10.0)
turbolinks (~> 2.5.0)
uglifier (~> 2.7.2)
underscore-rails (~> 1.4.4)
underscore-rails (~> 1.8.0)
unf (~> 0.1.4)
unicorn (~> 4.8.2)
unicorn-worker-killer (~> 0.4.2)
......
......@@ -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()
@renderFrequentlyUsedBlock()
@setupSearch()
addAward: (emoji) ->
emoji = @normilizeEmojiName(emoji)
@postEmoji emoji, =>
@addAwardToEmojiBar(emoji)
$(".emoji-menu").hide()
addAwardToEmojiBar: (emoji, custom_path = '') ->
addAwardToEmojiBar: (emoji) ->
@addEmojiToFrequentlyUsedList(emoji)
emoji = @normilizeEmojiName(emoji)
if @exist(emoji)
if @isActive(emoji)
......@@ -17,7 +33,7 @@ class @AwardsHandler
counter.parent().addClass("active")
@addMeToAuthorList(emoji)
else
@createEmoji(emoji, custom_path)
@createEmoji(emoji)
exist: (emoji) ->
@findEmojiIcon(emoji).length > 0
......@@ -27,15 +43,19 @@ class @AwardsHandler
decrementCounter: (emoji) ->
counter = @findEmojiIcon(emoji).siblings(".counter")
emojiIcon = counter.parent()
if parseInt(counter.text()) > 1
counter.text(parseInt(counter.text()) - 1)
counter.parent().removeClass("active")
emojiIcon.removeClass("active")
@removeMeFromAuthorList(emoji)
else if emoji =="thumbsup" || emoji == "thumbsdown"
emojiIcon.tooltip("destroy")
counter.text(0)
emojiIcon.removeClass("active")
else
award = counter.parent()
award.tooltip("destroy")
award.remove()
emojiIcon.tooltip("destroy")
emojiIcon.remove()
removeMeFromAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent()
......@@ -54,35 +74,39 @@ 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()
resolveNameToCssClass: (emoji) ->
emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']")
if emoji_icon.length > 0
unicodeName = emoji_icon.data("unicode-name")
else
$("li[data-emoji='" + emoji + "']").html()
# Find by alias
unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data("unicode-name")
"emoji-#{unicodeName}"
postEmoji: (emoji, callback) ->
$.post @post_emoji_url, { note: {
note: ":" + emoji + ":"
note: ":#{emoji}:"
noteable_type: @noteable_type
noteable_id: @noteable_id
}},(data) ->
......@@ -90,7 +114,7 @@ class @AwardsHandler
callback.call()
findEmojiIcon: (emoji) ->
$(".icon[data-emoji='" + emoji + "']")
$(".award [data-emoji='#{emoji}']")
scrollToAwards: ->
$('body, html').animate({
......@@ -99,3 +123,43 @@ class @AwardsHandler
normilizeEmojiName: (emoji) ->
@aliases[emoji] || emoji
addEmojiToFrequentlyUsedList: (emoji) ->
frequently_used_emojis = @getFrequentlyUsedEmojis()
frequently_used_emojis.push(emoji)
$.cookie('frequently_used_emojis', frequently_used_emojis.join(","), { expires: 365 })
getFrequentlyUsedEmojis: ->
frequently_used_emojis = ($.cookie('frequently_used_emojis') || "").split(",")
_.compact(_.uniq(frequently_used_emojis))
renderFrequentlyUsedBlock: ->
frequently_used_emojis = @getFrequentlyUsedEmojis()
ul = $("<ul>")
for emoji in frequently_used_emojis
do (emoji) ->
$(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul)
$("input.emoji-search").after(ul).after($("<h5>").text("Frequently used"))
setupSearch: ->
$("input.emoji-search").keyup (ev) =>
term = $(ev.target).val()
# Clean previous search results
$("ul.emoji-search,h5.emoji-search").remove()
if term
# Generate a search result block
h5 = $("<h5>").text("Search results").addClass("emoji-search")
found_emojis = @searchEmojis(term).show()
ul = $("<ul>").addClass("emoji-search").append(found_emojis)
$(".emoji-menu-content ul, .emoji-menu-content h5").hide()
$(".emoji-menu-content").append(h5).append(ul)
else
$(".emoji-menu-content").children().show()
searchEmojis: (term)->
$(".emoji-menu-content [data-emoji*='#{term}']").closest("li").clone()
......@@ -35,7 +35,7 @@ class @BlobFileDropzone
return
this.on 'sending', (file, xhr, formData) ->
formData.append('new_branch', form.find('.js-new-branch').val())
formData.append('target_branch', form.find('.js-target-branch').val())
formData.append('create_merge_request', form.find('.js-create-merge-request').val())
formData.append('commit_message', form.find('.js-commit-message').val())
return
......
......@@ -18,7 +18,7 @@ class @MergeRequestWidget
if data.state == "merged"
urlSuffix = if deleteSourceBranch then '?delete_source=true' else ''
window.location.href = window.location.href + urlSuffix
window.location.href = window.location.pathname + urlSuffix
else if data.merge_error
$('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>")
else
......
class @NewBranchForm
constructor: (form, availableRefs) ->
@branchNameError = form.find('.js-branch-name-error')
@name = form.find('.js-branch-name')
@ref = form.find('#ref')
@setupAvailableRefs(availableRefs)
@setupRestrictions()
@addBinding()
@init()
addBinding: ->
@name.on 'blur', @validate
init: ->
@name.trigger 'blur' if @name.val().length > 0
setupAvailableRefs: (availableRefs) ->
@ref.autocomplete
source: availableRefs,
minLength: 1
setupRestrictions: ->
startsWith = {
pattern: /^(\/|\.)/g,
prefix: "can't start with",
conjunction: "or"
}
endsWith = {
pattern: /(\/|\.|\.lock)$/g,
prefix: "can't end in",
conjunction: "or"
}
invalid = {
pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g
prefix: "can't contain",
conjunction: ", "
}
single = {
pattern: /^@+$/g
prefix: "can't be",
conjunction: "or"
}
@restrictions = [startsWith, invalid, endsWith, single]
validate: =>
@branchNameError.empty()
unique = (values, value) ->
values.push(value) unless value in values
values
formatter = (values, restriction) ->
formatted = values.map (value) ->
switch
when /\s/.test value then 'spaces'
when /\/{2,}/g.test value then 'consecutive slashes'
else "'#{value}'"
"#{restriction.prefix} #{formatted.join(restriction.conjunction)}"
validator = (errors, restriction) =>
matched = @name.val().match(restriction.pattern)
if matched
errors.concat formatter(matched.reduce(unique, []), restriction)
else
errors
errors = @restrictions.reduce validator, []
if errors.length > 0
errorMessage = $("<span/>").text(errors.join(', '))
@branchNameError.append(errorMessage)
class @NewCommitForm
constructor: (form) ->
@newBranch = form.find('.js-new-branch')
@newBranch = form.find('.js-target-branch')
@originalBranch = form.find('.js-original-branch')
@createMergeRequest = form.find('.js-create-merge-request')
@createMergeRequestContainer = form.find('.js-create-merge-request-container')
......
......@@ -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()
###
......
class @Project
constructor: ->
# Git protocol switcher
$('.js-protocol-switch').click ->
$('ul.clone-options-dropdown a').click ->
return if $(@).hasClass('active')
......@@ -10,7 +10,8 @@ class @Project
# Add the active class for the clicked button
$(@).toggleClass('active')
url = $(@).data('clone')
url = $("#project_clone").val()
console.log("url",url)
# Update the input field
$('#project_clone').val(url)
......
......@@ -8,17 +8,17 @@ class @ProjectsList
$(".projects-list-filter").keyup ->
terms = $(this).val()
uiBox = $(this).closest('.projects-list-holder')
uiBox = $('div.projects-list-holder')
if terms == "" || terms == undefined
uiBox.find(".projects-list li").show()
uiBox.find("ul.projects-list li").show()
else
uiBox.find(".projects-list li").each (index) ->
name = $(this).find(".filter-title").text()
uiBox.find("ul.projects-list li").each (index) ->
name = $(this).find("span.filter-title").text()
if name.toLowerCase().search(terms.toLowerCase()) == -1
$(this).hide()
else
$(this).show()
uiBox.find(".projects-list li.bottom").hide()
uiBox.find("ul.projects-list li.bottom").hide()
class @Star
constructor: ->
$('.project-home-panel .toggle-star').on('ajax:success', (e, data, status, xhr) ->
$this = $(this)
$starSpan = $this.find('span')
$starIcon = $this.find('i')
toggleStar = (isStarred) ->
$this.parent().find('span.count').text data.star_count
if isStarred
$starSpan.removeClass('starred').text 'Star'
$starIcon.removeClass('fa-star').addClass 'fa-star-o'
else
$starSpan.addClass('starred').text 'Unstar'
$starIcon.removeClass('fa-star-o').addClass 'fa-star'
return
toggleStar $starSpan.hasClass('starred')
return
).on 'ajax:error', (e, xhr, status, error) ->
new Flash('Star toggle failed. Try again later.', 'alert')
return
\ No newline at end of file
......@@ -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
......@@ -48,4 +48,4 @@
/*
* Styles for JS behaviors.
*/
@import "behaviors.scss";
\ No newline at end of file
@import "behaviors.scss";
@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 {
......
......@@ -383,7 +383,7 @@ table {
}
}
.center-top-menu {
.center-top-menu, .left-top-menu {
@include nav-menu;
text-align: center;
margin-top: 5px;
......@@ -417,6 +417,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;
}
......
......@@ -81,7 +81,7 @@
display: none;
}
.center-top-menu {
.center-top-menu, .left-top-menu {
li a {
font-size: 14px;
padding: 19px 10px;
......
......@@ -45,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;
......
......@@ -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,64 @@
}
}
.awards-menu {
padding: $gl-padding;
min-width: 214px;
.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;
width: 300px;
height: 300px;
overflow-y: scroll;
h5 {
clear: left;
}
> li {
cursor: pointer;
width: 30px;
height: 30px;
text-align: center;
@include border-radius(5px);
ul {
list-style-type: none;
margin-left: -20px;
margin-bottom: 20px;
overflow: auto;
}
img {
margin-bottom: 2px;
input.emoji-search{
background: image-url("icon-search.png") 240px no-repeat;
}
&:hover {
background-color: #ccc;
li {
cursor: pointer;
width: 30px;
height: 30px;
text-align: center;
float: left;
margin: 3px;
list-decorate: none;
@include border-radius(5px);
&: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: 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;
}
}
}
......
......@@ -91,10 +91,9 @@
}
}
.input-group {
.git-clone-holder {
display: inline-table;
position: relative;
top: 17px;
}
.project-repo-buttons {
......@@ -102,11 +101,74 @@
margin-bottom: 0px;
}
.count-buttons {
display: block;
margin-bottom: 12px;
}
.btn {
@include btn-gray;
text-transform: none;
}
.count-with-arrow {
display: inline-block;
position: relative;
margin-left: 4px;
.arrow {
&:before {
content: '';
display: inline-block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: 50%;
left: 0;
margin-top: -6px;
border-width: 7px 5px 7px 0;
border-right-color: #dce0e5;
}
&:after {
content: '';
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: 50%;
left: 1px;
margin-top: -9px;
border-width: 10px 7px 10px 0;
border-right-color: #FFF;
}
}
.count {
@include btn-gray;
display: inline-block;
background: white;
border-radius: 2px;
border-width: 1px;
border-style: solid;
font-size: 13px;
font-weight: 600;
line-height: 20px;
padding: 11px 16px;
letter-spacing: .4px;
padding: 10px;
text-align: center;
vertical-align: middle;
touch-action: manipulation;
cursor: pointer;
background-image: none;
white-space: nowrap;
margin: 0 11px 0px 4px;
&:hover {
background: #FFF;
}
}
}
}
......@@ -125,6 +187,13 @@
margin-right: 45px;
}
.clone-options {
display: table-cell;
a.btn {
width: 100%;
}
}
.form-control {
cursor: auto;
@extend .monospace;
......@@ -339,6 +408,38 @@ ul.nav.nav-projects-tabs {
}
}
.top-area {
border-bottom: 1px solid #EEE;
margin: 0 -16px;
padding: 0 $gl-padding;
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;
......@@ -416,11 +517,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 {
......
......@@ -49,6 +49,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:default_branch_protection,
:signup_enabled,
:signin_enabled,
:require_two_factor_authentication,
:two_factor_grace_period,
:gravatar_enabled,
:twitter_sharing_enabled,
:sign_in_text,
......
......@@ -13,6 +13,7 @@ class ApplicationController < ActionController::Base
before_action :validate_user_service_ticket!
before_action :reject_blocked!
before_action :check_password_expiration
before_action :check_2fa_requirement
before_action :ldap_security_check
before_action :default_headers
before_action :add_gon_variables
......@@ -223,6 +224,12 @@ class ApplicationController < ActionController::Base
end
end
def check_2fa_requirement
if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor?
redirect_to new_profile_two_factor_auth_path
end
end
def ldap_security_check
if current_user && current_user.requires_ldap_check?
unless Gitlab::LDAP::Access.allowed?(current_user)
......@@ -363,6 +370,23 @@ class ApplicationController < ActionController::Base
current_application_settings.import_sources.include?('git')
end
def two_factor_authentication_required?
current_application_settings.require_two_factor_authentication
end
def two_factor_grace_period
current_application_settings.two_factor_grace_period
end
def two_factor_grace_period_expired?
date = current_user.otp_grace_period_started_at
date && (date + two_factor_grace_period.hours) < Time.current
end
def skip_two_factor?
session[:skip_tfa] && session[:skip_tfa] > Time.current
end
def redirect_to_home_page_url?
# If user is not signed-in and tries to access root_path - redirect him to landing page
# Don't redirect to the default URL to prevent endless redirections
......
......@@ -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
module CreatesCommit
extend ActiveSupport::Concern
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
set_commit_variables
commit_params = @commit_params.merge(
source_project: @project,
source_branch: @ref,
target_branch: @target_branch
)
result = service.new(@tree_edit_project, current_user, commit_params).execute
if result[:status] == :success
flash[:notice] = success_notice || "Your changes have been successfully committed."
if create_merge_request?
success_path = new_merge_request_path
target = different_project? ? "project" : "branch"
flash[:notice] << " You can now submit a merge request to get this change into the original #{target}."
end
respond_to do |format|
format.html { redirect_to success_path }
format.json { render json: { message: "success", filePath: success_path } }
end
else
flash[:alert] = result[:message]
respond_to do |format|
format.html do
if failure_view
render failure_view
else
redirect_to failure_path
end
end
format.json { render json: { message: "failed", filePath: failure_path } }
end
end
end
def authorize_edit_tree!
return if can?(current_user, :push_code, project)
return if current_user && current_user.already_forked?(project)
access_denied!
end
private
def new_merge_request_path
new_namespace_project_merge_request_path(
@mr_source_project.namespace,
@mr_source_project,
merge_request: {
source_project_id: @mr_source_project.id,
target_project_id: @mr_target_project.id,
source_branch: @mr_source_branch,
target_branch: @mr_target_branch
}
)
end
def different_project?
@mr_source_project != @mr_target_project
end
def different_branch?
@mr_source_branch != @mr_target_branch || different_project?
end
def create_merge_request?
params[:create_merge_request].present? && different_branch?
end
def set_commit_variables
@mr_source_branch = @target_branch
if can?(current_user, :push_code, @project)
# Edit file in this project
@tree_edit_project = @project
@mr_source_project = @project
if @project.forked?
# Merge request from this project to fork origin
@mr_target_project = @project.forked_from_project
@mr_target_branch = @mr_target_project.repository.root_ref
else
# Merge request to this project
@mr_target_project = @project
@mr_target_branch = @ref
end
else
# Edit file in fork
@tree_edit_project = current_user.fork_of(@project)
# Merge request from fork to this project
@mr_source_project = @tree_edit_project
@mr_target_project = @project
@mr_target_branch = @mr_target_project.repository.root_ref
end
end
end
module CreatesMergeRequestForCommit
extend ActiveSupport::Concern
def new_merge_request_path
if @project.forked?
target_project = @project.forked_from_project || @project
target_branch = target_project.repository.root_ref
else
target_project = @project
target_branch = @ref
end
new_namespace_project_merge_request_path(
@project.namespace,
@project,
merge_request: {
source_project_id: @project.id,
target_project_id: target_project.id,
source_branch: @new_branch,
target_branch: target_branch
}
)
end
def create_merge_request?
params[:create_merge_request] && @new_branch != @ref
end
end
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_2fa_requirement
def new
unless current_user.otp_secret
current_user.otp_secret = User.generate_otp_secret(32)
current_user.save!
end
unless current_user.otp_grace_period_started_at && two_factor_grace_period
current_user.otp_grace_period_started_at = Time.current
end
current_user.save! if current_user.changed?
if two_factor_grace_period_expired?
flash.now[:alert] = 'You must configure Two-Factor Authentication in your account.'
else
grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
flash.now[:alert] = "You must configure Two-Factor Authentication in your account until #{l(grace_period_deadline)}."
end
@qr_code = build_qr_code
......@@ -34,6 +48,15 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
redirect_to profile_account_path
end
def skip
if two_factor_grace_period_expired?
redirect_to new_profile_two_factor_auth_path, alert: 'Cannot skip two factor authentication setup'
else
session[:skip_tfa] = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
redirect_to root_path
end
end
private
def build_qr_code
......
# Controller for viewing a file's blame
class Projects::BlobController < Projects::ApplicationController
include ExtractsPath
include CreatesMergeRequestForCommit
include CreatesCommit
include ActionView::Helpers::SanitizeHelper
# Raised when given an invalid file path
......@@ -9,21 +9,21 @@ class Projects::BlobController < Projects::ApplicationController
before_action :require_non_empty_project, except: [:new, :create]
before_action :authorize_download_code!
before_action :authorize_push_code!, only: [:destroy, :create]
before_action :authorize_edit_tree!, only: [:new, :create, :edit, :update, :destroy]
before_action :assign_blob_vars
before_action :commit, except: [:new, :create]
before_action :blob, except: [:new, :create]
before_action :from_merge_request, only: [:edit, :update]
before_action :require_branch_head, only: [:edit, :update]
before_action :editor_variables, except: [:show, :preview, :diff]
before_action :after_edit_path, only: [:edit, :update]
def new
commit unless @repository.empty?
end
def create
create_commit(Files::CreateService, success_path: after_create_path,
create_commit(Files::CreateService, success_notice: "The file has been successfully created.",
success_path: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)),
failure_view: :new,
failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref))
end
......@@ -36,6 +36,14 @@ class Projects::BlobController < Projects::ApplicationController
end
def update
after_edit_path =
if from_merge_request && @target_branch == @ref
diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
"#file-path-#{hexdigest(@path)}"
else
namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path))
end
create_commit(Files::UpdateService, success_path: after_edit_path,
failure_view: :edit,
failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
......@@ -50,15 +58,10 @@ class Projects::BlobController < Projects::ApplicationController
end
def destroy
result = Files::DeleteService.new(@project, current_user, @commit_params).execute
if result[:status] == :success
flash[:notice] = "Your changes have been successfully committed"
redirect_to after_destroy_path
else
flash[:alert] = result[:message]
render :show
end
create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.",
success_path: namespace_project_tree_path(@project.namespace, @project, @target_branch),
failure_view: :show,
failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
end
def diff
......@@ -108,74 +111,13 @@ class Projects::BlobController < Projects::ApplicationController
render_404
end
def create_commit(service, success_path:, failure_view:, failure_path:)
result = service.new(@project, current_user, @commit_params).execute
if result[:status] == :success
flash[:notice] = "Your changes have been successfully committed"
respond_to do |format|
format.html { redirect_to success_path }
format.json { render json: { message: "success", filePath: success_path } }
end
else
flash[:alert] = result[:message]
respond_to do |format|
format.html { render failure_view }
format.json { render json: { message: "failed", filePath: failure_path } }
end
end
end
def after_create_path
@after_create_path ||=
if create_merge_request?
new_merge_request_path
else
namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @file_path))
end
end
def after_edit_path
@after_edit_path ||=
if create_merge_request?
new_merge_request_path
elsif from_merge_request && @new_branch == @ref
diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
"#file-path-#{hexdigest(@path)}"
else
namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @path))
end
end
def after_destroy_path
@after_destroy_path ||=
if create_merge_request?
new_merge_request_path
else
namespace_project_tree_path(@project.namespace, @project, @new_branch)
end
end
def from_merge_request
# If blob edit was initiated from merge request page
@from_merge_request ||= MergeRequest.find_by(id: params[:from_merge_request_id])
end
def sanitized_new_branch_name
sanitize(strip_tags(params[:new_branch]))
end
def editor_variables
@current_branch = @ref
@new_branch =
if params[:new_branch].present?
sanitized_new_branch_name
elsif ::Gitlab::GitAccess.new(current_user, @project).can_push_to_branch?(@ref)
@ref
else
@repository.next_patch_branch
end
@target_branch = params[:target_branch]
@file_path =
if action_name.to_s == 'create'
......@@ -194,8 +136,6 @@ class Projects::BlobController < Projects::ApplicationController
@commit_params = {
file_path: @file_path,
current_branch: @current_branch,
target_branch: @new_branch,
commit_message: params[:commit_message],
file_content: params[:content],
file_content_encoding: params[:encoding]
......
......@@ -10,19 +10,35 @@ class Projects::ForksController < Projects::ApplicationController
def create
namespace = Namespace.find(params[:namespace_key])
@forked_project = ::Projects::ForkService.new(project, current_user, namespace: namespace).execute
@forked_project = namespace.projects.find_by(path: project.path)
@forked_project = nil unless @forked_project && @forked_project.forked_from_project == project
@forked_project ||= ::Projects::ForkService.new(project, current_user, namespace: namespace).execute
if @forked_project.saved? && @forked_project.forked?
if @forked_project.import_in_progress?
redirect_to namespace_project_import_path(@forked_project.namespace, @forked_project)
redirect_to namespace_project_import_path(@forked_project.namespace, @forked_project, continue: continue_params)
else
redirect_to(
namespace_project_path(@forked_project.namespace, @forked_project),
notice: 'Project was successfully forked.'
)
if continue_params
redirect_to continue_params[:to], notice: continue_params[:notice]
else
redirect_to namespace_project_path(@forked_project.namespace, @forked_project), notice: "The project was successfully forked."
end
end
else
render :error
end
end
private
def continue_params
continue_params = params[:continue]
if continue_params
continue_params.permit(:to, :notice, :notice_now)
else
nil
end
end
end
class Projects::ImportsController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project!
before_action :require_no_repo
before_action :require_no_repo, except: :show
before_action :redirect_if_progress, except: :show
def new
......@@ -22,21 +22,36 @@ class Projects::ImportsController < Projects::ApplicationController
end
def show
unless @project.import_in_progress?
if @project.import_finished?
redirect_to(project_path(@project)) and return
if @project.repository_exists? || @project.import_finished?
if continue_params
redirect_to continue_params[:to], notice: continue_params[:notice]
else
redirect_to(new_namespace_project_import_path(@project.namespace,
@project)) and return
redirect_to project_path(@project), notice: "The project was successfully forked."
end
elsif @project.import_failed?
redirect_to new_namespace_project_import_path(@project.namespace, @project)
else
if continue_params && continue_params[:notice_now]
flash.now[:notice] = continue_params[:notice_now]
end
# Render
end
end
private
def continue_params
continue_params = params[:continue]
if continue_params
continue_params.permit(:to, :notice, :notice_now)
else
nil
end
end
def require_no_repo
if @project.repository_exists? && !@project.import_in_progress?
redirect_to(namespace_project_path(@project.namespace, @project)) and return
redirect_to(namespace_project_path(@project.namespace, @project))
end
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)
......
# Controller for viewing a repository's file structure
class Projects::TreeController < Projects::ApplicationController
include ExtractsPath
include CreatesMergeRequestForCommit
include CreatesCommit
include ActionView::Helpers::SanitizeHelper
before_action :require_non_empty_project, except: [:new, :create]
before_action :assign_ref_vars
before_action :assign_dir_vars, only: [:create_dir]
before_action :authorize_download_code!
before_action :authorize_push_code!, only: [:create_dir]
before_action :authorize_edit_tree!, only: [:create_dir]
def show
return render_404 unless @repository.commit(@ref)
......@@ -34,44 +34,20 @@ class Projects::TreeController < Projects::ApplicationController
def create_dir
return render_404 unless @commit_params.values.all?
begin
result = Files::CreateDirService.new(@project, current_user, @commit_params).execute
message = result[:message]
rescue => e
message = e.to_s
end
if result && result[:status] == :success
flash[:notice] = "The directory has been successfully created"
respond_to do |format|
format.html { redirect_to after_create_dir_path }
end
else
flash[:alert] = message
respond_to do |format|
format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, @new_branch) }
end
end
create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.",
success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@target_branch, @dir_name)),
failure_path: namespace_project_tree_path(@project.namespace, @project, @ref))
end
private
def assign_dir_vars
@new_branch = params[:new_branch].present? ? sanitize(strip_tags(params[:new_branch])) : @ref
@target_branch = params[:target_branch]
@dir_name = File.join(@path, params[:dir_name])
@commit_params = {
file_path: @dir_name,
current_branch: @ref,
target_branch: @new_branch,
commit_message: params[:commit_message],
}
end
def after_create_dir_path
if create_merge_request?
new_merge_request_path
else
namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @dir_name))
end
end
end
......@@ -182,7 +182,7 @@ class ProjectsController < ApplicationController
@project.reload
render json: {
html: view_to_html_string("projects/buttons/_star")
star_count: @project.star_count
}
end
......
module AppearancesHelper
def brand_title
if brand_item
if brand_item && brand_item.title
brand_item.title
else
'GitLab Enterprise Edition'
......
......@@ -54,5 +54,17 @@ module AuthHelper
current_user.identities.exists?(provider: provider.to_s)
end
def two_factor_skippable?
current_application_settings.require_two_factor_authentication &&
!current_user.two_factor_enabled &&
current_application_settings.two_factor_grace_period &&
!two_factor_grace_period_expired?
end
def two_factor_grace_period_expired?
current_user.otp_grace_period_started_at &&
(current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current
end
extend self
end
......@@ -22,32 +22,90 @@ module BlobHelper
%w(credits changelog news copying copyright license authors)
end
def edit_blob_link(project, ref, path, options = {})
blob =
begin
project.repository.blob_at(ref, path)
rescue
nil
end
return unless blob && blob.text? && blob_editable?(blob)
text = 'Edit'
after = options[:after] || ''
def edit_blob_link(project = @project, ref = @ref, path = @path, options = {})
return unless current_user
blob = project.repository.blob_at(ref, path) rescue nil
return unless blob && blob_text_viewable?(blob)
from_mr = options[:from_merge_request_id]
link_opts = {}
link_opts[:from_merge_request_id] = from_mr if from_mr
cls = 'btn btn-small'
link_to(text,
namespace_project_edit_blob_path(project.namespace, project,
tree_join(ref, path),
link_opts),
class: cls
) + after.html_safe
edit_path = namespace_project_edit_blob_path(project.namespace, project,
tree_join(ref, path),
link_opts)
if !on_top_of_branch?
button_tag "Edit", class: "btn btn-default disabled has_tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
elsif can_edit_blob?(blob)
link_to "Edit", edit_path, class: 'btn btn-small'
elsif can?(current_user, :fork_project, project)
continue_params = {
to: edit_path,
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now
}
fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id,
continue: continue_params)
link_to "Edit", fork_path, class: 'btn btn-small', method: :post
end
end
def modify_file_link(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:)
return unless current_user
blob = project.repository.blob_at(ref, path) rescue nil
return unless blob
if !on_top_of_branch?
button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
elsif blob.lfs_pointer?
button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
elsif can_edit_blob?(blob)
button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
elsif can?(current_user, :fork_project, project)
continue_params = {
to: request.fullpath,
notice: edit_in_new_fork_notice + " Try to #{action} this file again.",
notice_now: edit_in_new_fork_notice_now
}
fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id,
continue: continue_params)
link_to label, fork_path, class: "btn btn-#{btn_class}", method: :post
end
end
def replace_blob_link(project = @project, ref = @ref, path = @path)
modify_file_link(
project,
ref,
path,
label: "Replace",
action: "replace",
btn_class: "default",
modal_type: "upload"
)
end
def delete_blob_link(project = @project, ref = @ref, path = @path)
modify_file_link(
project,
ref,
path,
label: "Delete",
action: "delete",
btn_class: "remove",
modal_type: "remove"
)
end
def blob_editable?(blob, project = @project, ref = @ref)
!blob.lfs_pointer? && allowed_tree_edit?(project, ref)
def can_edit_blob?(blob, project = @project, ref = @ref)
!blob.lfs_pointer? && can_edit_tree?(project, ref)
end
def leave_edit_message
......@@ -70,7 +128,7 @@ module BlobHelper
icon("#{file_type_icon_class('file', mode, name)} fw")
end
def blob_viewable?(blob)
def blob_text_viewable?(blob)
blob && blob.text? && !blob.lfs_pointer?
end
......
......@@ -94,11 +94,14 @@ 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, aliases = [])
unicode ||= Emoji.emoji_filename(name)
content_tag :div, "",
class: "icon emoji-icon emoji-#{unicode}",
"data-emoji" => name,
"data-aliases" => aliases.join(" "),
"data-unicode-name" => unicode
end
def emoji_author_list(notes, current_user)
......@@ -109,10 +112,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"
......@@ -121,6 +120,18 @@ module IssuesHelper
end
end
def awards_sort(awards)
awards.sort_by do |award, notes|
if award == "thumbsup"
0
elsif award == "thumbsdown"
1
else
2
end
end.to_h
end
def projects_weight_options(selected = nil)
options = (Issue::WEIGHT_RANGE).map do |i|
[i, i]
......
......@@ -8,6 +8,80 @@ module PageLayoutHelper
@page_title.join(" \u00b7 ")
end
# Define or get a description for the current page
#
# description - String (default: nil)
#
# If this helper is called multiple times with an argument, only the last
# description will be returned when called without an argument. Descriptions
# have newlines replaced with spaces and all HTML tags are sanitized.
#
# Examples:
#
# page_description # => "GitLab Community Edition"
# page_description("Foo")
# page_description # => "Foo"
#
# page_description("<b>Bar</b>\nBaz")
# page_description # => "Bar Baz"
#
# Returns an HTML-safe String.
def page_description(description = nil)
@page_description ||= page_description_default
if description.present?
@page_description = description.squish
else
sanitize(@page_description, tags: []).truncate_words(30)
end
end
# Default value for page_description when one hasn't been defined manually by
# a view
def page_description_default
if @project
@project.description || brand_title
else
brand_title
end
end
def page_image
default = image_url('gitlab_logo.png')
if @project
@project.avatar_url || default
elsif @user
avatar_icon(@user)
else
default
end
end
# Define or get attributes to be used as Twitter card metadata
#
# map - Hash of label => data pairs. Keys become labels, values become data
#
# Raises ArgumentError if given more than two attributes
def page_card_attributes(map = {})
raise ArgumentError, 'cannot provide more than two attributes' if map.length > 2
@page_card_attributes ||= {}
@page_card_attributes = map.reject { |_,v| v.blank? } if map.present?
@page_card_attributes
end
def page_card_meta_tags
tags = ''
page_card_attributes.each_with_index do |pair, i|
tags << tag(:meta, property: "twitter:label#{i + 1}", content: pair[0])
tags << tag(:meta, property: "twitter:data#{i + 1}", content: pair[1])
end
tags.html_safe
end
def header_title(title = nil, title_url = nil)
if title
@header_title = title
......
......@@ -50,24 +50,49 @@ module TreeHelper
project.repository.branch_names.include?(ref)
end
def allowed_tree_edit?(project = nil, ref = nil)
def can_edit_tree?(project = nil, ref = nil)
project ||= @project
ref ||= @ref
return false unless on_top_of_branch?(project, ref)
can?(current_user, :push_code, project)
can?(current_user, :push_code, project) ||
(current_user && current_user.already_forked?(project))
end
def tree_edit_branch(project = @project, ref = @ref)
if allowed_tree_edit?(project, ref)
if can_push_branch?(project, ref)
ref
else
project.repository.next_patch_branch
end
return unless can_edit_tree?(project, ref)
if can_push_branch?(project, ref)
ref
else
project = tree_edit_project(project)
project.repository.next_patch_branch
end
end
def tree_edit_project(project = @project)
if can?(current_user, :push_code, project)
project
elsif current_user && current_user.already_forked?(project)
current_user.fork_of(project)
end
end
def edit_in_new_fork_notice_now
"You're not allowed to make changes to this project directly." +
" A fork of this project is being created that you can make changes in, so you can submit a merge request."
end
def edit_in_new_fork_notice
"You're not allowed to make changes to this project directly." +
" A fork of this project has been created that you can make changes in, so you can submit a merge request."
end
def commit_in_fork_help
"A new branch will be created in your fork and a new merge request will be started."
end
def tree_breadcrumbs(tree, max_links = 2)
if @path.present?
part_path = ""
......
......@@ -69,7 +69,6 @@ module VisibilityLevelHelper
def skip_level?(form_model, level)
form_model.is_a?(Project) &&
form_model.forked? &&
!Gitlab::VisibilityLevel.allowed_fork_levels(form_model.forked_from_project.visibility_level).include?(level)
!form_model.visibility_level_allowed?(level)
end
end
......@@ -148,14 +148,14 @@ class Ability
end
def public_project_rules
project_guest_rules + [
@public_project_rules ||= project_guest_rules + [
:download_code,
:fork_project
]
end
def project_guest_rules
[
@project_guest_rules ||= [
:read_project,
:read_wiki,
:read_issue,
......@@ -173,7 +173,7 @@ class Ability
end
def project_report_rules
project_guest_rules + [
@project_report_rules ||= project_guest_rules + [
:create_commit_status,
:read_commit_statuses,
:download_code,
......@@ -186,7 +186,7 @@ class Ability
end
def project_dev_rules
project_report_rules + [
@project_dev_rules ||= project_report_rules + [
:admin_merge_request,
:create_merge_request,
:create_wiki,
......@@ -197,7 +197,7 @@ class Ability
end
def project_archived_rules
[
@project_archived_rules ||= [
:create_merge_request,
:push_code,
:push_code_to_protected_branches,
......@@ -207,7 +207,7 @@ class Ability
end
def project_master_rules
project_dev_rules + [
@project_master_rules ||= project_dev_rules + [
:push_code_to_protected_branches,
:update_project_snippet,
:update_merge_request,
......@@ -222,7 +222,7 @@ class Ability
end
def project_admin_rules
project_master_rules + [
@project_admin_rules ||= project_master_rules + [
:change_namespace,
:change_visibility_level,
:rename_project,
......@@ -353,7 +353,7 @@ class Ability
end
if snippet.public? || snippet.internal?
rules << :read_personal_snippet
rules << :read_personal_snippet
end
rules
......
......@@ -2,33 +2,35 @@
#
# Table name: application_settings
#
# id :integer not null, primary key
# default_projects_limit :integer
# signup_enabled :boolean
# signin_enabled :boolean
# gravatar_enabled :boolean
# sign_in_text :text
# created_at :datetime
# updated_at :datetime
# home_page_url :string(255)
# default_branch_protection :integer default(2)
# twitter_sharing_enabled :boolean default(TRUE)
# help_text :text
# restricted_visibility_levels :text
# version_check_enabled :boolean default(TRUE)
# max_attachment_size :integer default(10), not null
# default_project_visibility :integer
# default_snippet_visibility :integer
# restricted_signup_domains :text
# user_oauth_applications :boolean default(TRUE)
# after_sign_out_path :string(255)
# session_expire_delay :integer default(10080), not null
# import_sources :text
# help_page_text :text
# admin_notification_email :string(255)
# shared_runners_enabled :boolean default(TRUE), not null
# max_artifacts_size :integer default(100), not null
# runners_registration_token :string(255)
# id :integer not null, primary key
# default_projects_limit :integer
# signup_enabled :boolean
# signin_enabled :boolean
# gravatar_enabled :boolean
# sign_in_text :text
# created_at :datetime
# updated_at :datetime
# home_page_url :string(255)
# default_branch_protection :integer default(2)
# twitter_sharing_enabled :boolean default(TRUE)
# help_text :text
# restricted_visibility_levels :text
# version_check_enabled :boolean default(TRUE)
# max_attachment_size :integer default(10), not null
# default_project_visibility :integer
# default_snippet_visibility :integer
# restricted_signup_domains :text
# user_oauth_applications :boolean default(TRUE)
# after_sign_out_path :string(255)
# session_expire_delay :integer default(10080), not null
# import_sources :text
# help_page_text :text
# admin_notification_email :string(255)
# shared_runners_enabled :boolean default(TRUE), not null
# max_artifacts_size :integer default(100), not null
# runners_registration_token :string(255)
# require_two_factor_authentication :boolean default(TRUE)
# two_factor_grace_period :integer default(48)
#
class ApplicationSetting < ActiveRecord::Base
......@@ -59,6 +61,9 @@ class ApplicationSetting < ActiveRecord::Base
allow_blank: true,
email: true
validates :two_factor_grace_period,
numericality: { greater_than_or_equal_to: 0 }
validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil?
value.each do |level|
......@@ -113,6 +118,8 @@ class ApplicationSetting < ActiveRecord::Base
import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
two_factor_grace_period: 48
)
end
......@@ -135,4 +142,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
......@@ -161,6 +161,14 @@ module Issuable
self.class.to_s.underscore
end
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
'Author' => author.try(:name),
'Assignee' => assignee.try(:name)
}
end
def notes_with_associations
notes.includes(:author, :project)
end
......
......@@ -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
end
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)
......
......@@ -16,7 +16,7 @@ class GlobalMilestone
end
def safe_title
@title.to_slug.to_s
@title.to_slug.normalize.to_s
end
def expired?
......
......@@ -12,6 +12,7 @@
class Identity < ActiveRecord::Base
include Sortable
include CaseSensitivity
belongs_to :user
validates :provider, presence: true
......
......@@ -107,9 +107,16 @@ class Note < ActiveRecord::Base
end
def grouped_awards
notes = {}
awards.select(:note).distinct.map do |note|
[ note.note, where(note: note.note) ]
notes[note.note] = where(note: note.note)
end
notes["thumbsup"] ||= Note.none
notes["thumbsdown"] ||= Note.none
notes
end
end
......
......@@ -66,6 +66,19 @@ class Project < ActiveRecord::Base
after_destroy :remove_pages
# update visibility_levet of forks
after_update :update_forks_visibility_level
def update_forks_visibility_level
return unless visibility_level < visibility_level_was
forks.each do |forked_project|
if forked_project.visibility_level > visibility_level
forked_project.visibility_level = visibility_level
forked_project.save!
end
end
end
ActsAsTaggableOn.strict_case_match = true
acts_as_taggable_on :tags
......@@ -106,10 +119,12 @@ class Project < ActiveRecord::Base
has_one :gitlab_issue_tracker_service, dependent: :destroy
has_one :external_wiki_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
has_many :forked_project_links, foreign_key: "forked_from_project_id"
has_many :forks, through: :forked_project_links, source: :forked_to_project
has_one :forked_from_project, through: :forked_project_link
# Merge Requests for target project should be removed with it
has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id'
# Merge requests from source project should be kept when source project was removed
......@@ -868,7 +883,7 @@ class Project < ActiveRecord::Base
end
def forks_count
ForkedProjectLink.where(forked_from_project_id: self.id).count
forks.count
end
def find_label(name)
......@@ -985,6 +1000,11 @@ class Project < ActiveRecord::Base
issues.opened.count
end
def visibility_level_allowed?(level)
return true unless forked?
Gitlab::VisibilityLevel.allowed_fork_levels(forked_from_project.visibility_level).include?(level.to_i)
end
def pages_url
if Dir.exist?(public_pages_path)
host = "#{namespace.path}.#{Settings.pages.host}"
......
......@@ -648,47 +648,54 @@ class Repository
Gitlab::Popen.popen(args, path_to_repo)
end
def commit_with_hooks(current_user, branch)
oldrev = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
was_empty = empty?
# Create temporary ref
def with_tmp_ref(oldrev = nil)
random_string = SecureRandom.hex
tmp_ref = "refs/tmp/#{random_string}/head"
unless was_empty
oldrev = find_branch(branch).target
if oldrev && !Gitlab::Git.blank_ref?(oldrev)
rugged.references.create(tmp_ref, oldrev)
end
# Make commit in tmp ref
newrev = yield(tmp_ref)
yield(tmp_ref)
ensure
rugged.references.delete(tmp_ref) rescue nil
end
def commit_with_hooks(current_user, branch)
oldrev = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
was_empty = empty?
unless newrev
raise CommitError.new('Failed to create commit')
unless was_empty
oldrev = find_branch(branch).target
end
GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
if was_empty
# Create branch
rugged.references.create(ref, newrev)
else
# Update head
current_head = find_branch(branch).target
with_tmp_ref(oldrev) do |tmp_ref|
# Make commit in tmp ref
newrev = yield(tmp_ref)
unless newrev
raise CommitError.new('Failed to create commit')
end
# Make sure target branch was not changed during pre-receive hook
if current_head == oldrev
rugged.references.update(ref, newrev)
GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
if was_empty
# Create branch
rugged.references.create(ref, newrev)
else
raise CommitError.new('Commit was rejected because branch received new push')
# Update head
current_head = find_branch(branch).target
# Make sure target branch was not changed during pre-receive hook
if current_head == oldrev
rugged.references.update(ref, newrev)
else
raise CommitError.new('Commit was rejected because branch received new push')
end
end
end
end
rescue GitHooksService::PreReceiveError
# Remove tmp ref and return error to user
rugged.references.delete(tmp_ref)
raise
end
private
......
......@@ -39,10 +39,7 @@ class BaseService
def deny_visibility_level(model, denied_visibility_level = nil)
denied_visibility_level ||= model.visibility_level
level_name = 'Unknown'
Gitlab::VisibilityLevel.options.each do |name, level|
level_name = name if level == denied_visibility_level
end
level_name = Gitlab::VisibilityLevel.level_name(denied_visibility_level)
model.errors.add(
:visibility_level,
......
require_relative 'base_service'
class CreateBranchService < BaseService
def execute(branch_name, ref)
def execute(branch_name, ref, source_project: @project)
valid_branch = Gitlab::GitRefValidator.validate(branch_name)
if valid_branch == false
return error('Branch name invalid')
return error('Branch name is invalid')
end
repository = project.repository
......@@ -13,7 +13,20 @@ class CreateBranchService < BaseService
return error('Branch already exists')
end
new_branch = repository.add_branch(current_user, branch_name, ref)
new_branch = nil
if source_project != @project
repository.with_tmp_ref do |tmp_ref|
repository.fetch_ref(
source_project.repository.path_to_repo,
"refs/heads/#{ref}",
tmp_ref
)
new_branch = repository.add_branch(current_user, branch_name, tmp_ref)
end
else
new_branch = repository.add_branch(current_user, branch_name, ref)
end
if new_branch
push_data = build_push_data(project, current_user, new_branch)
......
......@@ -3,8 +3,10 @@ module Files
class ValidationError < StandardError; end
def execute
@current_branch = params[:current_branch]
@source_project = params[:source_project] || @project
@source_branch = params[:source_branch]
@target_branch = params[:target_branch]
@commit_message = params[:commit_message]
@file_path = params[:file_path]
@file_content = if params[:file_content_encoding] == 'base64'
......@@ -16,8 +18,8 @@ module Files
# Validate parameters
validate
# Create new branch if it different from current_branch
if @target_branch != @current_branch
# Create new branch if it different from source_branch
if different_branch?
create_target_branch
end
......@@ -26,18 +28,14 @@ module Files
else
error("Something went wrong. Your changes were not committed")
end
rescue Repository::CommitError, GitHooksService::PreReceiveError, ValidationError => ex
rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, ValidationError => ex
error(ex.message)
end
private
def current_branch
@current_branch ||= params[:current_branch]
end
def target_branch
@target_branch ||= params[:target_branch]
def different_branch?
@source_branch != @target_branch || @source_project != @project
end
def raise_error(message)
......@@ -52,11 +50,11 @@ module Files
end
unless project.empty_repo?
unless repository.branch_names.include?(@current_branch)
unless @source_project.repository.branch_names.include?(@source_branch)
raise_error("You can only create or edit files when you are on a branch")
end
if @current_branch != @target_branch
if different_branch?
if repository.branch_names.include?(@target_branch)
raise_error("Branch with such name already exists. You need to switch to this branch in order to make changes")
end
......@@ -65,10 +63,10 @@ module Files
end
def create_target_branch
result = CreateBranchService.new(project, current_user).execute(@target_branch, @current_branch)
result = CreateBranchService.new(project, current_user).execute(@target_branch, @source_branch, source_project: @source_project)
unless result[:status] == :success
raise_error("Something went wrong when we tried to create #{@target_branch} for you")
raise_error("Something went wrong when we tried to create #{@target_branch} for you: #{result[:message]}")
end
end
......
......@@ -26,7 +26,7 @@ module Files
unless project.empty_repo?
@file_path.slice!(0) if @file_path.start_with?('/')
blob = repository.blob_at_branch(@current_branch, @file_path)
blob = repository.blob_at_branch(@source_branch, @file_path)
if blob
raise_error("Your changes could not be committed because a file with the same name already exists")
......
......@@ -3,12 +3,16 @@ module Projects
def execute
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
if new_visibility && new_visibility.to_i != project.visibility_level
unless can?(current_user, :change_visibility_level, project) &&
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(project, new_visibility)
return project
if new_visibility
if new_visibility.to_i != project.visibility_level
unless can?(current_user, :change_visibility_level, project) &&
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(project, new_visibility)
return project
end
end
return false unless visibility_level_allowed?(new_visibility)
end
new_branch = params[:default_branch]
......@@ -23,5 +27,19 @@ module Projects
end
end
end
private
def visibility_level_allowed?(level)
return true if project.visibility_level_allowed?(level)
level_name = Gitlab::VisibilityLevel.level_name(level)
project.errors.add(
:visibility_level,
"#{level_name} could not be set as visibility level of this project - parent project settings are more restrictive"
)
false
end
end
end
......@@ -104,6 +104,18 @@
= f.label :signin_enabled do
= f.check_box :signin_enabled
Sign-in enabled
.form-group
= f.label :two_factor_authentication, 'Two-Factor authentication', class: 'control-label col-sm-2'
.col-sm-10
.checkbox
= f.label :require_two_factor_authentication do
= f.check_box :require_two_factor_authentication
Require all users to setup Two-Factor authentication
.form-group
= f.label :two_factor_authentication, 'Two-Factor grace period (hours)', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
.help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
.form-group
= f.label :restricted_signup_domains, 'Restricted domains for sign-ups', class: 'control-label col-sm-2'
.col-sm-10
......
......@@ -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
= text_area_tag :content, nil, class: 'form-control span1', rows: 7, require: true
.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'
.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
= nav_link(page: starred_dashboard_projects_path) do
= link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do
Starred Projects
= 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
%ul.center-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
= nav_link(page: starred_dashboard_projects_path) do
= link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do
Starred Projects
= 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
......@@ -6,7 +6,7 @@
- else
= render 'explore/head'
.gray-content-block.clearfix
.gray-content-block.clearfix.second-block
= render 'filter'
= render 'projects', projects: @projects
= paginate @projects, theme: "gitlab"
......@@ -7,7 +7,7 @@
= render 'explore/head'
.explore-trending-block
.gray-content-block
.gray-content-block.second-block
.pull-right
= render 'explore/projects/dropdown'
.oneline
......
......@@ -7,7 +7,7 @@
= render 'explore/head'
.explore-trending-block
.gray-content-block
.gray-content-block.second-block
.pull-right
= render 'explore/projects/dropdown'
.oneline
......
- page_title "GitLab"
%head
%head{prefix: "og: http://ogp.me/ns#"}
%meta{charset: "utf-8"}
%meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'}
%meta{content: "GitLab Enterprise Edition", name: "description"}
%meta{name: 'referrer', content: 'origin-when-cross-origin'}
%meta{name: "description", content: page_description}
-# Open Graph - http://ogp.me/
%meta{property: 'og:type', content: "object"}
%meta{property: 'og:site_name', content: "GitLab"}
%meta{property: 'og:title', content: page_title}
%meta{property: 'og:description', content: page_description}
%meta{property: 'og:image', content: page_image}
%meta{property: 'og:url', content: request.base_url + request.fullpath}
-# Twitter Card - https://dev.twitter.com/cards/types/summary
%meta{property: 'twitter:card', content: "summary"}
%meta{property: 'twitter:title', content: page_title}
%meta{property: 'twitter:description', content: page_description}
%meta{property: 'twitter:image', content: page_image}
= page_card_meta_tags
- page_title "GitLab"
%title= page_title
= favicon_link_tag 'favicon.ico'
......
......@@ -12,6 +12,6 @@
comment = val.match(/^\S+ \S+ (.+)\n?$/);
if( comment && comment.length > 1 && title.val() == '' ){
$('#key_title').val( comment[1] );
$('#key_title').val( comment[1] ).change();
}
});
......@@ -38,3 +38,4 @@
= text_field_tag :pin_code, nil, class: "form-control", required: true, autofocus: true
.form-actions
= submit_tag 'Submit', class: 'btn btn-success'
= link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable?
......@@ -2,3 +2,7 @@
= button_tag 'Commit Changes', class: 'btn commit-btn js-commit-button btn-create'
= link_to 'Cancel', cancel_path,
class: 'btn btn-cancel', data: {confirm: leave_edit_message}
- unless can?(current_user, :push_code, @project)
.inline.prepend-left-10
= commit_in_fork_help
......@@ -34,7 +34,7 @@
= icon('rss')
.project-repo-buttons
.split-one
.split-one.count-buttons
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
......@@ -46,3 +46,6 @@
= render 'projects/buttons/dropdown'
= render 'projects/buttons/notifications'
:coffeescript
new Star()
\ No newline at end of file
......@@ -2,7 +2,7 @@
= link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id),
class: 'btn btn-sm', target: '_blank'
-# only show normal/blame view links for text files
- if blob_viewable?(@blob)
- if blob_text_viewable?(@blob)
- if current_page? namespace_project_blame_path(@project.namespace, @project, @id)
= link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id),
class: 'btn btn-sm'
......@@ -14,13 +14,8 @@
= link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project,
tree_join(@commit.sha, @path)), class: 'btn btn-sm'
- if blob_editable?(@blob)
- if current_user
.btn-group{ role: "group" }
= edit_blob_link(@project, @ref, @path)
%button.btn.btn-default{ 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } Replace
%button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Delete
- elsif !on_top_of_branch?
.btn-group{ role: "group" }
%button.btn.btn-default.disabled.has_tooltip{title: "You can only edit files when you are on a branch.", data: {container: 'body'}} Edit
%button.btn.btn-default.disabled.has_tooltip{title: "You can only replace files when you are on a branch.", data: {container: 'body'}} Replace
%button.btn.btn-remove.disabled.has_tooltip{title: "You can only delete files when you are on a branch.", data: {container: 'body'}} Delete
= edit_blob_link
= replace_blob_link
= delete_blob_link
......@@ -17,5 +17,9 @@
= submit_tag "Create directory", class: 'btn btn-create'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- unless can?(current_user, :push_code, @project)
.inline.prepend-left-10
= commit_in_fork_help
:javascript
new NewCommitForm($('.js-create-dir-form'))
......@@ -20,6 +20,11 @@
= button_tag button_title, class: 'btn btn-small btn-create btn-upload-file', id: 'submit-all'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- unless can?(current_user, :push_code, @project)
.inline.prepend-left-10
= commit_in_fork_help
:javascript
disableButtonIfEmptyField($('.js-upload-blob-form').find('.js-commit-message'), '.btn-upload-file');
new BlobFileDropzone($('.js-upload-blob-form'), '#{method}');
......
......@@ -20,7 +20,7 @@
= hidden_field_tag 'last_commit', @last_commit
= hidden_field_tag 'content', '', id: "file-content"
= hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id]
= render 'projects/commit_button', ref: @ref, cancel_path: @after_edit_path
= render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id)
:javascript
blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}")
......
......@@ -6,7 +6,7 @@
%div#tree-holder.tree-holder
= render 'blob', blob: @blob
- if blob_editable?(@blob)
- if can_edit_blob?(@blob)
= render 'projects/blob/remove'
- title = "Replace #{@blob.name}"
......
......@@ -9,11 +9,12 @@
New Branch
%hr
= form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal js-requires-input" do
= form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal js-create-branch-form js-requires-input" do
.form-group
= label_tag :branch_name, nil, class: 'control-label'
.col-sm-10
= text_field_tag :branch_name, params[:branch_name], required: true, tabindex: 1, autofocus: true, class: 'form-control'
= text_field_tag :branch_name, params[:branch_name], required: true, tabindex: 1, autofocus: true, class: 'form-control js-branch-name'
.help-block.text-danger.js-branch-name-error
.form-group
= label_tag :ref, 'Create from', class: 'control-label'
.col-sm-10
......@@ -26,7 +27,4 @@
:javascript
var availableRefs = #{@project.repository.ref_names.to_json};
$("#ref").autocomplete({
source: availableRefs,
minLength: 1
});
new NewBranchForm($('.js-create-branch-form'), availableRefs)
......@@ -18,10 +18,11 @@
= link_to new_namespace_project_snippet_path(@project.namespace, @project) do
= icon('file-text-o fw')
New snippet
- if can?(current_user, :push_code, @project)
%li.divider
%li
= link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'), title: 'New file' do
= link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do
= icon('file fw')
New file
%li
......@@ -32,3 +33,20 @@
= link_to new_namespace_project_tag_path(@project.namespace, @project) do
= icon('tags fw')
New tag
- elsif current_user && current_user.already_forked?(@project)
%li.divider
%li
= link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do
= icon('file fw')
New file
- elsif can?(current_user, :fork_project, @project)
%li.divider
%li
- continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now }
- fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
= icon('file fw')
New file
......@@ -4,10 +4,15 @@
= link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has_tooltip' do
= icon('code-fork fw')
Fork
%div.count-with-arrow
%span.arrow
%span.count
= @project.forks_count
- else
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has_tooltip' do
= icon('code-fork fw')
Fork
%div.count-with-arrow
%span.arrow
%span.count
= @project.forks_count
- if current_user
= link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star has_tooltip', method: :post, remote: true, title: "Star project" do
= icon('star fw')
%span.count
- if current_user.starred?(@project)
= icon('star fw')
%span.starred Unstar
- else
= icon('star-o fw')
%span Star
%div.count-with-arrow
%span.arrow
%span.count.star-count
= @project.star_count
:javascript
$('.project-home-panel .toggle-star').on('ajax:success', function (e, data, status, xhr) {
$(this).replaceWith(data.html);
})
.on('ajax:error', function (e, xhr, status, error) {
new Flash('Star toggle failed. Try again later.', 'alert');
});
- else
= link_to new_user_session_path, class: 'btn has_tooltip star-btn', title: 'You must sign in to star a project' do
= icon('star fw')
Star
%div.count-with-arrow
%span.arrow
%span.count
= @project.star_count
......@@ -24,7 +24,7 @@
= "#{diff_file.diff.a_mode}#{diff_file.diff.b_mode}"
.diff-controls
- if blob_viewable?(blob)
- if blob_text_viewable?(blob)
= link_to '#', class: 'js-toggle-diff-comments btn btn-sm active has_tooltip', title: "Toggle comments for this file" do
%i.fa.fa-comments
&nbsp;
......@@ -32,14 +32,15 @@
- if editable_diff?(diff_file)
= edit_blob_link(@merge_request.source_project,
@merge_request.source_branch, diff_file.new_path,
after: '&nbsp;', from_merge_request_id: @merge_request.id)
from_merge_request_id: @merge_request.id)
&nbsp;
= view_file_btn(diff_commit.id, diff_file, project)
.diff-content.diff-wrap-lines
-# Skipp all non non-supported blobs
- return unless blob.respond_to?('text?')
- if blob_viewable?(blob)
- if blob_text_viewable?(blob)
- if diff_view == 'parallel'
= render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
- else
......
......@@ -43,4 +43,3 @@
%i.fa.fa-spinner.fa-spin
Forking repository
%p Please wait a moment, this page will automatically refresh when ready.
- 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'
- page_title "#{@issue.title} (##{@issue.iid})", "Issues"
- page_title "#{@issue.title} (##{@issue.iid})", "Issues"
- page_description @issue.description
- page_card_attributes @issue.card_attributes
= render "header_title"
.issue
......@@ -23,16 +26,16 @@
.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)
- if @issue.closed?
= link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-grouped btn-reopen'
= link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-reopen'
- else
= link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close', title: 'Close Issue'
= link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-close', 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,7 +17,7 @@
- if merge_request.open? && merge_request.broken?
%li
= link_to merge_request_path(merge_request), class: "has_tooltip", title: "Cannot be merged automatically", data: {container: 'body'} do
= link_to merge_request_path(merge_request), class: "has_tooltip", title: "Cannot be merged automatically", data: { container: 'body' } do
= icon('exclamation-triangle')
- if merge_request.assignee
......
- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
= render "header_title"
- if params[:view] == 'parallel'
......
......@@ -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'
- page_title @milestone.title, "Milestones"
- page_title @milestone.title, "Milestones"
- page_description @milestone.description
= render "header_title"
.detail-page-header
......
......@@ -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
......@@ -29,7 +29,7 @@
- if tree.readme
= render "projects/tree/readme", readme: tree.readme
- if allowed_tree_edit?
- if can_edit_tree?
= render 'projects/blob/upload', title: 'Upload New File', placeholder: 'Upload new file', button_title: 'Upload file', form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post
= render 'projects/blob/new_dir'
......
......@@ -11,34 +11,65 @@
= link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path)
- else
= link_to title, '#'
- if allowed_tree_edit?
- if current_user
%li
%span.dropdown
%a.dropdown-toggle.btn.btn-sm.add-to-tree{href: '#', "data-toggle" => "dropdown"}
- if !on_top_of_branch?
%span.btn.btn-sm.add-to-tree.disabled.has_tooltip{title: "You can only add files when you are on a branch", data: { container: 'body' }}
= icon('plus')
%ul.dropdown-menu
%li
= link_to namespace_project_new_blob_path(@project.namespace, @project, @id), title: 'Create file', id: 'new-file-link' do
= icon('pencil fw')
New file
%li
= link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do
= icon('file fw')
Upload file
%li
= link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do
= icon('folder fw')
New directory
%li.divider
%li
= link_to new_namespace_project_branch_path(@project.namespace, @project) do
= icon('code-fork fw')
New branch
%li
= link_to new_namespace_project_tag_path(@project.namespace, @project) do
= icon('tags fw')
New tag
- elsif !on_top_of_branch?
%li
%span.btn.btn-sm.add-to-tree.disabled.has_tooltip{title: "You can only add files when you are on a branch.", data: {container: 'body'}}
= icon('plus')
- else
%span.dropdown
%a.dropdown-toggle.btn.btn-sm.add-to-tree{href: '#', "data-toggle" => "dropdown"}
= icon('plus')
%ul.dropdown-menu
- if can_edit_tree?
%li
= link_to namespace_project_new_blob_path(@project.namespace, @project, @id) do
= icon('pencil fw')
New file
%li
= link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do
= icon('file fw')
Upload file
%li
= link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do
= icon('folder fw')
New directory
- elsif can?(current_user, :fork_project, @project)
%li
- continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @id),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now }
- fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
= icon('pencil fw')
New file
%li
- continue_params = { to: request.fullpath,
notice: edit_in_new_fork_notice + " Try to upload a file again.",
notice_now: edit_in_new_fork_notice_now }
- fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
= icon('file fw')
Upload file
%li
- continue_params = { to: request.fullpath,
notice: edit_in_new_fork_notice + " Try to create a new directory again.",
notice_now: edit_in_new_fork_notice_now }
- fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
= icon('folder fw')
New directory
%li.divider
%li
= link_to new_namespace_project_branch_path(@project.namespace, @project) do
= icon('code-fork fw')
New branch
%li
= link_to new_namespace_project_tag_path(@project.namespace, @project) do
= icon('tags fw')
New tag
......@@ -6,7 +6,7 @@
- if issue.description.present?
.description.term
= preserve do
= search_md_sanitize(markdown(issue.description))
= search_md_sanitize(markdown(issue.description, { project: issue.project }))
%span.light
#{issue.project.name_with_namespace}
- if issue.closed?
......
- project = project || @project
.git-clone-holder.input-group
.input-group-addon.git-protocols
.input-group-btn
= ssh_clone_button(project)
.input-group-btn
= http_clone_button(project)
- if alternative_kerberos_url?
.input-group-btn
= kerberos_clone_button(project)
.git-clone-holder
.btn-group.clone-options
%a#clone-dropdown.clone-dropdown-btn.btn{href: '#', 'data-toggle' => 'dropdown'}
%span
= default_clone_protocol.upcase
= icon('angle-down')
%ul.dropdown-menu.dropdown-menu-right.clone-options-dropdown
%li
%a#ssh-selector{href: @project.ssh_url_to_repo}
SSH
%li
%a#http-selector{href: @project.http_url_to_repo}
HTTPS
- if alternative_kerberos_url?
%li
%a#kerberos-btn{href: @project.kerberos_url_to_repo}
KRB5
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true
.input-group-btn
= clipboard_button(clipboard_target: '#project_clone')
:javascript
$('ul.clone-options-dropdown a').on('click',function(e){
e.preventDefault();
var $this = $(this);
$('a.clone-dropdown-btn span').text($this.text());
$('#project_clone').val($this.attr('href'));
});
= render 'shared/commit_message_container', placeholder: placeholder
- unless @project.empty_repo?
.form-group.branch
= label_tag 'new_branch', 'Target branch', class: 'control-label'
.col-sm-10
= text_field_tag 'new_branch', @new_branch || tree_edit_branch, required: true, class: "form-control js-new-branch"
- if @project.empty_repo?
= hidden_field_tag 'target_branch', @ref
- else
- if can?(current_user, :push_code, @project)
.form-group.branch
= label_tag 'target_branch', 'Target branch', class: 'control-label'
.col-sm-10
= text_field_tag 'target_branch', @target_branch || tree_edit_branch, required: true, class: "form-control js-target-branch"
.js-create-merge-request-container
.checkbox
- nonce = SecureRandom.hex
= label_tag "create_merge_request-#{nonce}" do
= check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}"
Start a <strong>new merge request</strong> with these changes
.js-create-merge-request-container
.checkbox
- nonce = SecureRandom.hex
= label_tag "create_merge_request-#{nonce}" do
= check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}"
Start a <strong>new merge request</strong> with these changes
- else
= hidden_field_tag 'target_branch', @target_branch || tree_edit_branch
= hidden_field_tag 'create_merge_request', 1
= hidden_field_tag 'original_branch', @ref, class: 'js-original-branch'
- page_title @user.name
- header_title @user.name, user_path(@user)
- page_title @user.name
- page_description @user.bio
- header_title @user.name, user_path(@user)
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
......
.awards.votes-block
- votable.notes.awards.grouped_awards.each do |emoji, notes|
- awards_sort(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
= text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
- AwardEmoji.emoji_by_category.each do |category, emojis|
%h5= AwardEmoji::CATEGORIES[category]
%ul
- emojis.each do |emoji|
%li
= emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
- if current_user
:coffeescript
post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}"
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)
aliases = #{AwardEmoji.aliases.to_json}
$(".awards-menu li").click (e)->
emoji = $(this).data("emoji")
window.awards_handler = new AwardsHandler(
post_emoji_url,
noteable_type,
noteable_id,
aliases
)
$(".awards").on "click", ".emoji-menu-content li", (e) ->
emoji = $(this).find(".emoji-icon").data("emoji")
awards_handler.addAward(emoji)
$(".awards").on "click", ".award", (e)->
$(".awards").on "click", ".award", (e) ->
emoji = $(this).find(".icon").data("emoji")
awards_handler.addAward(emoji)
$(".award").tooltip()
$(".emoji-menu-content").niceScroll({cursorwidth: "7px", autohidemode: false})
......@@ -316,6 +316,7 @@ Rails.application.routes.draw do
resource :two_factor_auth, only: [:new, :create, :destroy] do
member do
post :codes
patch :skip
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 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
......
class AddTfaToApplicationSettings < ActiveRecord::Migration
def change
change_table :application_settings do |t|
t.boolean :require_two_factor_authentication, default: false
t.integer :two_factor_grace_period, default: 48
end
end
end
class AddTfaAdditionalFields < ActiveRecord::Migration
def change
change_table :users do |t|
t.datetime :otp_grace_period_started_at, null: true
end
end
end
# Migration type: online without errors (works on previous version and new one)
class RenameEmojis < ActiveRecord::Migration
def up
# Renames aliases to main names
execute("UPDATE notes SET note ='thumbsup' WHERE is_award = true AND note = '+1'")
execute("UPDATE notes SET note ='thumbsdown' WHERE is_award = true AND note = '-1'")
execute("UPDATE notes SET note ='poop' WHERE is_award = true AND note = 'shit'")
end
def down
execute("UPDATE notes SET note ='+1' WHERE is_award = true AND note = 'thumbsup'")
execute("UPDATE notes SET note ='-1' WHERE is_award = true AND note = 'thumbsdown'")
execute("UPDATE notes SET note ='shit' WHERE is_award = true AND note = 'poop'")
end
end
......@@ -43,26 +43,28 @@ ActiveRecord::Schema.define(version: 20151228203337) do
t.text "sign_in_text"
t.datetime "created_at"
t.datetime "updated_at"
t.string "home_page_url", limit: 255
t.integer "default_branch_protection", default: 2
t.boolean "twitter_sharing_enabled", default: true
t.string "home_page_url", limit: 255
t.integer "default_branch_protection", default: 2
t.boolean "twitter_sharing_enabled", default: true
t.text "help_text"
t.text "restricted_visibility_levels"
t.boolean "version_check_enabled", default: true
t.integer "max_attachment_size", default: 10, null: false
t.boolean "version_check_enabled", default: true
t.integer "max_attachment_size", default: 10, null: false
t.integer "default_project_visibility"
t.integer "default_snippet_visibility"
t.text "restricted_signup_domains"
t.boolean "user_oauth_applications", default: true
t.string "after_sign_out_path", limit: 255
t.integer "session_expire_delay", default: 10080, null: false
t.boolean "user_oauth_applications", default: true
t.string "after_sign_out_path", limit: 255
t.integer "session_expire_delay", default: 10080, null: false
t.text "import_sources"
t.text "help_page_text"
t.string "admin_notification_email", limit: 255
t.boolean "shared_runners_enabled", default: true, null: false
t.integer "max_artifacts_size", default: 100, null: false
t.string "admin_notification_email", limit: 255
t.boolean "shared_runners_enabled", default: true, null: false
t.integer "max_artifacts_size", default: 100, null: false
t.string "runners_registration_token"
t.integer "max_pages_size", default: 100, null: false
t.integer "max_pages_size", default: 100, null: false
t.boolean "require_two_factor_authentication", default: false
t.integer "two_factor_grace_period", default: 48
end
create_table "approvals", force: :cascade do |t|
......@@ -929,6 +931,7 @@ ActiveRecord::Schema.define(version: 20151228203337) do
t.text "note"
t.boolean "hide_project_limit", default: false
t.string "unlock_token"
t.datetime "otp_grace_period_started_at"
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
......
......@@ -29,17 +29,18 @@
- [Using SSH keys](ci/ssh_keys/README.md)
- [User permissions](ci/permissions/README.md)
- [API](ci/api/README.md)
- [Triggering builds through the API](ci/triggers/README.md)
### CI Languages
+ [Testing PHP](ci/languages/php.md)
- [Testing PHP](ci/languages/php.md)
### CI Services
+ [Using MySQL](ci/services/mysql.md)
+ [Using PostgreSQL](ci/services/postgres.md)
+ [Using Redis](ci/services/redis.md)
+ [Using Other Services](ci/docker/using_docker_images.md#how-to-use-other-images-as-services)
- [Using MySQL](ci/services/mysql.md)
- [Using PostgreSQL](ci/services/postgres.md)
- [Using Redis](ci/services/redis.md)
- [Using Other Services](ci/docker/using_docker_images.md#how-to-use-other-images-as-services)
### CI Examples
......
......@@ -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
}
......
......@@ -92,7 +92,17 @@ GET /users
You can search for users by email or username with: `/users?search=John`
Also see `def search query` in `app/models/user.rb`.
In addition, you can lookup users by username:
```
GET /users?username=:username
```
For example:
```
GET /users?username=jack_smith
```
## Single user
......
......@@ -2,28 +2,30 @@
### User documentation
+ [Quick Start](quick_start/README.md)
+ [Configuring project (.gitlab-ci.yml)](yaml/README.md)
+ [Configuring runner](runners/README.md)
+ [Configuring deployment](deployment/README.md)
+ [Using Docker Images](docker/using_docker_images.md)
+ [Using Docker Build](docker/using_docker_build.md)
+ [Using Variables](variables/README.md)
+ [Using SSH keys](ssh_keys/README.md)
* [Quick Start](quick_start/README.md)
* [Configuring project (.gitlab-ci.yml)](yaml/README.md)
* [Configuring runner](runners/README.md)
* [Configuring deployment](deployment/README.md)
* [Using Docker Images](docker/using_docker_images.md)
* [Using Docker Build](docker/using_docker_build.md)
* [Using Variables](variables/README.md)
* [Using SSH keys](ssh_keys/README.md)
* [Triggering builds through the API](triggers/README.md)
### Languages
+ [Testing PHP](languages/php.md)
* [Testing PHP](languages/php.md)
### Services
+ [Using MySQL](services/mysql.md)
+ [Using PostgreSQL](services/postgres.md)
+ [Using Redis](services/redis.md)
+ [Using Other Services](docker/using_docker_images.md#how-to-use-other-images-as-services)
* [Using MySQL](services/mysql.md)
* [Using PostgreSQL](services/postgres.md)
* [Using Redis](services/redis.md)
* [Using Other Services](docker/using_docker_images.md#how-to-use-other-images-as-services)
### 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)
......@@ -31,5 +33,5 @@
### Administrator documentation
+ [User permissions](permissions/README.md)
+ [API](api/README.md)
* [User permissions](permissions/README.md)
* [API](api/README.md)
# Triggering Builds through the API
_**Note:** This feature was [introduced][ci-229] in GitLab CE 7.14_
Triggers can be used to force a rebuild of a specific branch, tag or commit,
with an API call.
## Add a trigger
You can add a new trigger by going to your project's **Settings > Triggers**.
The **Add trigger** button will create a new token which you can then use to
trigger a rebuild of this particular project.
Once at least one trigger is created, on the **Triggers** page you will find
some descriptive information on how you can
Every new trigger you create, gets assigned a different token which you can
then use inside your scripts or `.gitlab-ci.yml`. You also have a nice
overview of the time the triggers were last used.
![Triggers page overview](img/triggers_page.png)
## Revoke a trigger
You can revoke a trigger any time by going at your project's
**Settings > Triggers** and hitting the **Revoke** button. The action is
irreversible.
## Trigger a build
To trigger a build you need to send a `POST` request to GitLab's API endpoint:
```
POST /projects/:id/trigger/builds
```
The required parameters are the trigger's `token` and the Git `ref` on which
the trigger will be performed. Valid refs are the branch, the tag or the commit
SHA. The `:id` of a project can be found by [querying the API](../api/projects.md)
or by visiting the **Triggers** page which provides self-explanatory examples.
When a rebuild is triggered, the information is exposed in GitLab's UI under
the **Builds** page and the builds are marked as `triggered`.
![Marked rebuilds as triggered on builds page](img/builds_page.png)
---
You can see which trigger caused the rebuild by visiting the single build page.
The token of the trigger is exposed in the UI as you can see from the image
below.
![Marked rebuilds as triggered on a single build page](img/trigger_single_build.png)
---
See the [Examples](#examples) section for more details on how to actually
trigger a rebuild.
## Pass build variables to a trigger
You can pass any number of arbitrary variables in the trigger API call and they
will be available in GitLab CI so that they can be used in your `.gitlab-ci.yml`
file. The parameter is of the form:
```
variables[key]=value
```
This information is also exposed in the UI.
![Build variables in UI](img/trigger_variables.png)
---
See the [Examples](#examples) section below for more details.
## Examples
Using cURL you can trigger a rebuild with minimal effort, for example:
```bash
curl -X POST \
-F token=TOKEN \
-F ref=master \
https://gitlab.example.com/api/v3/projects/9/trigger/builds
```
In this case, the project with ID `9` will get rebuilt on `master` branch.
### Triggering a build within `.gitlab-ci.yml`
You can also benefit by using triggers in your `.gitlab-ci.yml`. Let's say that
you have two projects, A and B, and you want to trigger a rebuild on the `master`
branch of project B whenever a tag on project A is created. This is the job you
need to add in project's A `.gitlab-ci.yml`:
```yaml
build_docs:
stage: deploy
script:
- "curl -X POST -F token=TOKEN -F ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds"
only:
- tags
```
Now, whenever a new tag is pushed on project A, the build will run and the
`build_docs` job will be executed, triggering a rebuild of project B. The
`stage: deploy` ensures that this job will run only after all jobs with
`stage: test` complete successfully.
_**Note:** If your project is public, passing the token in plain text is
probably not the wiser idea, so you might want to use a
[secure variable](../variables/README.md#user-defined-variables-secure-variables)
for that purpose._
### Making use of trigger variables
Using trigger variables can be proven useful for a variety of reasons.
* Identifiable jobs. Since the variable is exposed in the UI you can know
why the rebuild was triggered if you pass a variable that explains the
purpose.
* Conditional job processing. You can have conditional jobs that run whenever
a certain variable is present.
Consider the following `.gitlab-ci.yml` where we set three
[stages](../yaml/README.md#stages) and the `upload_package` job is run only
when all jobs from the test and build stages pass. When the `UPLOAD_TO_S3`
variable is non-zero, `make upload` is run.
```yaml
stages:
- test
- build
- package
run_tests:
script:
- make test
build_package:
stage: build
script:
- make build
upload_package:
stage: package
script:
- if [ -n "${UPLOAD_TO_S3}" ]; then make upload; fi
```
You can then trigger a rebuild while you pass the `UPLOAD_TO_S3` variable
and the script of the `upload_package` job will run:
```bash
curl -X POST \
-F token=TOKEN \
-F ref=master \
-F "variables[UPLOAD_TO_S3]=true" \
https://gitlab.example.com/api/v3/projects/9/trigger/builds
```
### Using cron to trigger nightly builds
Whether you craft a script or just run cURL directly, you can trigger builds
in conjunction with cron. The example below triggers a build on the `master`
branch of project with ID `9` every night at `00:30`:
```bash
30 0 * * * curl -X POST -F token=TOKEN -F ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds
```
[ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229
......@@ -6,3 +6,5 @@
- [Information exclusivity](information_exclusivity.md)
- [Reset your root password](reset_root_password.md)
- [User File Uploads](user_file_uploads.md)
- [How we manage the CRIME vulnerability](crime_vulnerability.md)
- [Enforce Two-Factor authentication](two_factor_authentication.md)
# How we manage the TLS protocol CRIME vulnerability
> CRIME ("Compression Ratio Info-leak Made Easy") is a security exploit against
secret web cookies over connections using the HTTPS and SPDY protocols that also
use data compression. When used to recover the content of secret
authentication cookies, it allows an attacker to perform session hijacking on an
authenticated web session, allowing the launching of further attacks.
([CRIME](https://en.wikipedia.org/w/index.php?title=CRIME&oldid=692423806))
### Description
The TLS Protocol CRIME Vulnerability affects compression over HTTPS, therefore
it warns against using SSL Compression (for example gzip) or SPDY which
optionally uses compression as well.
GitLab supports both gzip and [SPDY][ngx-spdy] and mitigates the CRIME
vulnerability by deactivating gzip when HTTPS is enabled. You can see the
sources of the files in question:
* [Source installation NGINX file][source-nginx]
* [Omnibus installation NGINX file][omnibus-nginx]
Although SPDY is enabled in Omnibus installations, CRIME relies on compression
(the 'C') and the default compression level in NGINX's SPDY module is 0
(no compression).
### Nessus
The Nessus scanner, [reports a possible CRIME vulnerability][nessus] in GitLab
similar to the following format:
```
Description
This remote service has one of two configurations that are known to be required for the CRIME attack:
SSL/TLS compression is enabled.
TLS advertises the SPDY protocol earlier than version 4.
...
Output
The following configuration indicates that the remote service may be vulnerable to the CRIME attack:
SPDY support earlier than version 4 is advertised.
```
From the report above it is important to note that Nessus is only checking if
TLS advertises the SPDY protocol earlier than version 4, it does not perform an
attack nor does it check if compression is enabled. With just this approach, it
cannot tell that SPDY's compression is disabled and not subject to the CRIME
vulnerability.
### References
* Nginx ["Module ngx_http_spdy_module"][ngx-spdy]
* Tenable Network Security, Inc. ["Transport Layer Security (TLS) Protocol CRIME Vulnerability"][nessus]
* Wikipedia contributors, ["CRIME"][wiki-crime] Wikipedia, The Free Encyclopedia
[source-nginx]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/gitlab-ssl
[omnibus-nginx]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/templates/default/nginx-gitlab-http.conf.erb
[ngx-spdy]: http://nginx.org/en/docs/http/ngx_http_spdy_module.html
[nessus]: https://www.tenable.com/plugins/index.php?view=single&id=62565
[wiki-crime]: https://en.wikipedia.org/wiki/CRIME
# Enforce Two-factor Authentication (2FA)
Two-factor Authentication (2FA) provides an additional level of security to your
users' GitLab account. Once enabled, in addition to supplying their username and
password to login, they'll be prompted for a code generated by an application on
their phone.
You can read more about it here:
[Two-factor Authentication (2FA)](doc/profile/two_factor_authentication.md)
## Enabling 2FA
Users on GitLab, can enable it without any admin's intervention. If you want to
enforce everyone to setup 2FA, you can choose from two different ways:
1. Enforce on next login
2. Suggest on next login, but allow a grace period before enforcing.
In the Admin area under **Settings** (`/admin/application_settings`), look for
the "Sign-in Restrictions" area, where you can configure both.
If you want 2FA enforcement to take effect on next login, change the grace
period to `0`
## Disabling 2FA for everyone
There may be some special situations where you want to disable 2FA for everyone
even when forced 2FA is disabled. There is a rake task for that:
```
# use this command if you've installed GitLab with the Omnibus package
sudo gitlab-rake gitlab:two_factor:disable_for_all_users
# if you've installed GitLab from source
sudo -u git -H bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
```
**IMPORTANT: this is a permanent and irreversible action. Users will have to reactivate 2FA from scratch if they want to use it again.**
......@@ -25,6 +25,7 @@ Feature: Project Commits Branches
And I click branch 'improve/awesome' delete link
Then I should not see branch 'improve/awesome'
@javascript
Scenario: I create a branch with invalid name
Given I visit project branches page
And I click new branch link
......
@project-create
Feature: Project Create
In order to get access to project sections
A user with ability to create a project
......
......@@ -13,7 +13,18 @@ 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 can search emoji
Given I click to emoji-picker
And I search "hand"
Then I see search result for "hand"
@javascript
Scenario: I add award emoji using regular comment
Given I leave comment with a single emoji
Then I have award added
Given I leave comment with a single emoji
Then I have award added
......@@ -12,6 +12,14 @@ Feature: Project Merge Requests Acceptance
Then I should see merge request merged
And I should not see the Remove Source Branch button
@javascript
Scenario: Accepting the Merge Request when URL has an anchor
Given I am on the Merge Request detail with note anchor page
When I click on "Remove source branch" option
And I click on Accept Merge Request
Then I should see merge request merged
And I should not see the Remove Source Branch button
@javascript
Scenario: Accepting the Merge Request without removing the source branch
Given I am on the Merge Request detail page
......
......@@ -24,6 +24,12 @@ Feature: Project Source Browse Files
Given I click on "New file" link in repo
Then I can see new file page
Scenario: I can create file when I don't have write access
Given I don't have write access
And I click on "New file" link in repo
Then I should see a notice about a new fork having been created
Then I can see new file page
@javascript
Scenario: I can create and commit file
Given I click on "New file" link in repo
......@@ -34,6 +40,17 @@ Feature: Project Source Browse Files
Then I am redirected to the new file
And I should see its new content
@javascript
Scenario: I can create and commit file when I don't have write access
Given I don't have write access
And I click on "New file" link in repo
And I edit code
And I fill the new file name
And I fill the commit message
And I click on "Commit Changes"
Then I am redirected to the fork's new merge request page
And I can see the new commit message
@javascript
Scenario: I can create and commit file with new lines at the end of file
Given I click on "New file" link in repo
......@@ -45,6 +62,17 @@ Feature: Project Source Browse Files
And I click button "Edit"
And I should see its content with new lines preserved at end of file
@javascript
Scenario: I can create and commit file and specify new branch
Given I click on "New file" link in repo
And I edit code
And I fill the new file name
And I fill the commit message
And I fill the new branch name
And I click on "Commit Changes"
Then I am redirected to the new merge request page
And I should see its new content
@javascript
Scenario: I can upload file and commit
Given I click on "Upload file" link in repo
......@@ -56,6 +84,19 @@ Feature: Project Source Browse Files
And I am redirected to the new merge request page
And I can see the new commit message
@javascript
Scenario: I can upload file and commit when I don't have write access
Given I don't have write access
And I click on "Upload file" link in repo
Then I should see a notice about a new fork having been created
When I click on "Upload file" link in repo
And I upload a new text file
And I fill the upload file commit message
And I click on "Upload file"
Then I can see the new text file
And I am redirected to the fork's new merge request page
And I can see the new commit message
@javascript
Scenario: I can replace file and commit
Given I click on ".gitignore" file in repo
......@@ -68,15 +109,19 @@ Feature: Project Source Browse Files
And I can see the replacement commit message
@javascript
Scenario: I can create and commit file and specify new branch
Given I click on "New file" link in repo
And I edit code
And I fill the new file name
And I fill the commit message
And I fill the new branch name
And I click on "Commit Changes"
Then I am redirected to the new merge request page
And I should see its new content
Scenario: I can replace file and commit when I don't have write access
Given I don't have write access
And I click on ".gitignore" file in repo
And I see the ".gitignore"
And I click on "Replace"
Then I should see a notice about a new fork having been created
When I click on "Replace"
And I replace it with a text file
And I fill the replace file commit message
And I click on "Replace file"
Then I can see the new text file
And I am redirected to the fork's new merge request page
And I can see the replacement commit message
@javascript
Scenario: I can create file in empty repo
......@@ -117,6 +162,14 @@ Feature: Project Source Browse Files
And I click button "Edit"
Then I can edit code
@javascript
Scenario: I can edit file when I don't have write access
Given I don't have write access
And I click on ".gitignore" file in repo
And I click button "Edit"
Then I should see a notice about a new fork having been created
And I can edit code
Scenario: If the file is binary the edit link is hidden
Given I visit a binary file in the repo
Then I cannot see the edit button
......@@ -131,6 +184,17 @@ Feature: Project Source Browse Files
Then I am redirected to the ".gitignore"
And I should see its new content
@javascript
Scenario: I can edit and commit file when I don't have write access
Given I don't have write access
And I click on ".gitignore" file in repo
And I click button "Edit"
And I edit code
And I fill the commit message
And I click on "Commit Changes"
Then I am redirected to the fork's new merge request page
And I can see the new commit message
@javascript
Scenario: I can edit and commit file to new branch
Given I click on ".gitignore" file in repo
......@@ -161,6 +225,17 @@ Feature: Project Source Browse Files
And I click on "Create directory"
Then I am redirected to the new merge request page
@javascript
Scenario: I can create directory in repo when I don't have write access
Given I don't have write access
When I click on "New directory" link in repo
Then I should see a notice about a new fork having been created
When I click on "New directory" link in repo
And I fill the new directory name
And I fill the commit message
And I click on "Create directory"
Then I am redirected to the fork's new merge request page
@javascript
Scenario: I attempt to create an existing directory
When I click on "New directory" link in repo
......@@ -188,6 +263,19 @@ Feature: Project Source Browse Files
Then I am redirected to the files URL
And I don't see the ".gitignore"
@javascript
Scenario: I can delete file and commit when I don't have write access
Given I don't have write access
And I click on ".gitignore" file in repo
And I see the ".gitignore"
And I click on "Delete"
Then I should see a notice about a new fork having been created
When I click on "Delete"
And I fill the commit message
And I click on "Delete file"
Then I am redirected to the fork's new merge request page
And I can see the new commit message
Scenario: I can browse directory with Browse Dir
Given I click on files directory
And I click on History link
......
@project-stars
Feature: Project Star
Scenario: New projects have 0 stars
Given public project "Community"
......
......@@ -61,7 +61,8 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
end
step 'I should see new an error that branch is invalid' do
expect(page).to have_content 'Branch name invalid'
expect(page).to have_content 'Branch name is invalid'
expect(page).to have_content "can't contain spaces"
end
step 'I should see new an error that ref is invalid' do
......
......@@ -36,7 +36,8 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps
end
step 'I click on HTTP' do
click_button 'HTTP'
find('#clone-dropdown').click
find('#http-selector').click
end
step 'Remote url should update to http link' do
......@@ -44,7 +45,8 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps
end
step 'If I click on SSH' do
click_button 'SSH'
find('#clone-dropdown').click
find('#ssh-selector').click
end
step 'Remote url should update to ssh link' do
......@@ -52,7 +54,8 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps
end
step 'If I click on KRB5' do
click_button 'KRB5'
find('#clone-dropdown').click
find('#kerberos-btn').click
end
step 'Remote url should update to kerberos link' do
......
......@@ -15,22 +15,31 @@ 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-content' do
page.first('.emoji-icon').click
end
end
step 'I can remove it by clicking to icon' do
page.within '.awards' do
page.first('.award').click
expect(page).to_not have_selector '.award'
expect do
page.find('.award.active').click
sleep 0.1
end.to change{ page.all(".award").size }.from(3).to(2)
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'
expect(page.find('.award .counter')).to have_content '1'
expect(page.find('.award.active .counter')).to have_content '1'
end
end
......@@ -45,4 +54,16 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
click_button 'Add Comment'
end
end
step 'I search "hand"' do
page.within('.emoji-menu-content') do
fill_in 'emoji_search', with: 'hand'
end
end
step 'I see search result for "hand"' do
page.within '.emoji-menu-content' do
expect(page).to have_selector '[data-emoji="raised_hand"]'
end
end
end
......@@ -6,6 +6,10 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
visit merge_request_path(@merge_request)
end
step 'I am on the Merge Request detail with note anchor page' do
visit merge_request_path(@merge_request, anchor: 'note_123')
end
step 'I click on "Remove source branch" option' do
check('Remove source branch')
end
......
......@@ -5,6 +5,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
include SharedPaths
include RepoHelpers
step "I don't have write access" do
@project = create(:project, name: "Other Project", path: "other-project")
@project.team << [@user, :reporter]
visit namespace_project_tree_path(@project.namespace, @project, root_ref)
end
step 'I should see files from repository' do
expect(page).to have_content "VERSION"
expect(page).to have_content ".gitignore"
......@@ -75,7 +81,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I fill the new branch name' do
fill_in :new_branch, with: 'new_branch_name', visible: true
fill_in :target_branch, with: 'new_branch_name', visible: true
end
step 'I fill the new file name with an illegal name' do
......@@ -87,7 +93,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I fill the commit message' do
fill_in :commit_message, with: 'Not yet a commit message.', visible: true
fill_in :commit_message, with: 'New commit message', visible: true
end
step 'I click link "Diff"' do
......@@ -103,7 +109,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I click on "Delete"' do
click_button 'Delete'
click_on 'Delete'
end
step 'I click on "Delete file"' do
......@@ -111,7 +117,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I click on "Replace"' do
click_button "Replace"
click_on "Replace"
end
step 'I click on "Replace file"' do
......@@ -124,7 +130,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I click on "New file" link in repo' do
find('.add-to-tree').click
click_link 'Create file'
click_link 'New file'
end
step 'I click on "Upload file" link in repo' do
......@@ -155,7 +161,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I can see the new commit message' do
expect(page).to have_content "New upload commit message"
expect(page).to have_content "New commit message"
end
step 'I upload a new text file' do
......@@ -164,7 +170,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I fill the upload file commit message' do
page.within('#modal-upload-blob') do
fill_in :commit_message, with: 'New upload commit message'
fill_in :commit_message, with: 'New commit message'
end
end
......@@ -251,9 +257,14 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
expect(current_path).to eq(new_namespace_project_merge_request_path(@project.namespace, @project))
end
step "I am redirected to the fork's new merge request page" do
fork = @user.fork_of(@project)
expect(current_path).to eq(new_namespace_project_merge_request_path(fork.namespace, fork))
end
step 'I am redirected to the root directory' do
expect(current_path).to eq(
namespace_project_tree_path(@project.namespace, @project, 'master/'))
namespace_project_tree_path(@project.namespace, @project, 'master'))
end
step "I don't see the permalink link" do
......@@ -332,8 +343,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
expect(page).to have_content 'Permalink'
expect(page).not_to have_content 'Edit'
expect(page).not_to have_content 'Blame'
expect(page).not_to have_content 'Delete'
expect(page).not_to have_content 'Replace'
expect(page).to have_content 'Delete'
expect(page).to have_content 'Replace'
end
step 'I should see a notice about a new fork having been created' do
expect(page).to have_content "You're not allowed to make changes to this project directly. A fork of this project has been created that you can make changes in, so you can submit a merge request."
end
private
......
......@@ -32,6 +32,6 @@ class Spinach::Features::ProjectStar < Spinach::FeatureSteps
protected
def has_n_stars(n)
expect(page).to have_css(".star-btn .count", text: n, visible: true)
expect(page).to have_css(".star-count", text: n, visible: true)
end
end
{
"northeast_pointing_airplane":"airplane_northeast",
"small_airplane":"airplane_small",
"up_pointing_small_airplane":"airplane_small_up",
"up_pointing_airplane":"airplane_up",
"left_anger_bubble":"anger_left",
"right_anger_bubble":"anger_right",
"ballot_box_with_ballot":"ballot_box",
"ballot_box_with_bold_check":"ballot_box_check",
"ballot_box_with_script_x":"ballot_box_x",
"ballot_script_x":"ballot_x",
"beach_with_umbrella":"beach",
"bellhop_bell":"bellhop",
"bouquet_of_flowers":"bouquet2",
"bullhorn_with_sound_waves":"bullhorn_waves",
"pocket calculator":"calculator",
"spiral_calendar_pad":"calendar_spiral",
"card_file_box":"card_box",
"tape_cartridge":"cartridge",
"city_sunrise":"city_sunset",
"mantlepiece_clock":"clock",
"clockwise_right_and_left_semicircle_arrows":"clockwise_arrows",
"cloud_with_lightning":"cloud_lightning",
"cloud_with_rain":"cloud_rain",
"cloud_with_snow":"cloud_snow",
"cloud_with_tornado":"cloud_tornado",
"old_personal_computer":"computer_old",
"building_construction":"contruction_site",
"couch_and_lamp":"couch",
"couple_with_heart_mm":"couple_mm",
"couple_with_heart_ww":"couple_ww",
"lower_left_crayon":"crayon",
"heavy_latin_cross":"cross_heavy",
"white_latin_cross":"cross_white",
"black_skull_and_crossbones":"crossbones",
"passenger_ship":"cruise_ship",
"dagger_knife":"dagger",
"desktop_computer":"desktop",
"card_index_dividers":"dividers",
"document_with_text":"document_text",
"dove_of_peace":"dove",
"email":"e-mail",
"back_of_envelope":"envelope_back",
"flying_envelope":"envelope_flying",
"stamped_envelope":"envelope_stamped",
"pen_over_stamped_envelope":"envelope_stamped_pen",
"white_down_pointing_left_hand_index":"finger_pointing_down",
"sideways_white_down_pointing_index":"finger_pointing_down2",
"sideways_white_left_pointing_index":"finger_pointing_left",
"sideways_white_right_pointing_index":"finger_pointing_right",
"sideways_white_up_pointing_index":"finger_pointing_up",
"flame":"fire",
"oncoming_fire_engine":"fire_engine_oncoming",
"ac":"flag_ac",
"ad":"flag_ad",
"ae":"flag_ae",
"af":"flag_af",
"ag":"flag_ag",
"ai":"flag_ai",
"al":"flag_al",
"am":"flag_am",
"ao":"flag_ao",
"ar":"flag_ar",
"at":"flag_at",
"au":"flag_au",
"aw":"flag_aw",
"az":"flag_az",
"ba":"flag_ba",
"bb":"flag_bb",
"bd":"flag_bd",
"be":"flag_be",
"bf":"flag_bf",
"bg":"flag_bg",
"bh":"flag_bh",
"bi":"flag_bi",
"bj":"flag_bj",
"waving_black_flag":"flag_black",
"bm":"flag_bm",
"bn":"flag_bn",
"bo":"flag_bo",
"br":"flag_br",
"bs":"flag_bs",
"bt":"flag_bt",
"bw":"flag_bw",
"by":"flag_by",
"bz":"flag_bz",
"ca":"flag_ca",
"congo":"flag_cd",
"cf":"flag_cf",
"cg":"flag_cg",
"ch":"flag_ch",
"ci":"flag_ci",
"chile":"flag_cl",
"cm":"flag_cm",
"cn":"flag_cn",
"co":"flag_co",
"cr":"flag_cr",
"cu":"flag_cu",
"cv":"flag_cv",
"cy":"flag_cy",
"cz":"flag_cz",
"de":"flag_de",
"dj":"flag_dj",
"dk":"flag_dk",
"dm":"flag_dm",
"do":"flag_do",
"dz":"flag_dz",
"ec":"flag_ec",
"ee":"flag_ee",
"eg":"flag_eg",
"eh":"flag_eh",
"er":"flag_er",
"es":"flag_es",
"et":"flag_et",
"fi":"flag_fi",
"fj":"flag_fj",
"fk":"flag_fk",
"fm":"flag_fm",
"fo":"flag_fo",
"fr":"flag_fr",
"ga":"flag_ga",
"gb":"flag_gb",
"gd":"flag_gd",
"ge":"flag_ge",
"gh":"flag_gh",
"gi":"flag_gi",
"gl":"flag_gl",
"gm":"flag_gm",
"gn":"flag_gn",
"gq":"flag_gq",
"gr":"flag_gr",
"gt":"flag_gt",
"gu":"flag_gu",
"gw":"flag_gw",
"gy":"flag_gy",
"hk":"flag_hk",
"hn":"flag_hn",
"hr":"flag_hr",
"ht":"flag_ht",
"hu":"flag_hu",
"indonesia":"flag_id",
"ie":"flag_ie",
"il":"flag_il",
"in":"flag_in",
"iq":"flag_iq",
"ir":"flag_ir",
"is":"flag_is",
"it":"flag_it",
"je":"flag_je",
"jm":"flag_jm",
"jo":"flag_jo",
"jp":"flag_jp",
"ke":"flag_ke",
"kg":"flag_kg",
"kh":"flag_kh",
"ki":"flag_ki",
"km":"flag_km",
"kn":"flag_kn",
"kp":"flag_kp",
"kr":"flag_kr",
"kw":"flag_kw",
"ky":"flag_ky",
"kz":"flag_kz",
"la":"flag_la",
"lb":"flag_lb",
"lc":"flag_lc",
"li":"flag_li",
"lk":"flag_lk",
"lr":"flag_lr",
"ls":"flag_ls",
"lt":"flag_lt",
"lu":"flag_lu",
"lv":"flag_lv",
"ly":"flag_ly",
"ma":"flag_ma",
"mc":"flag_mc",
"md":"flag_md",
"me":"flag_me",
"mg":"flag_mg",
"mh":"flag_mh",
"mk":"flag_mk",
"ml":"flag_ml",
"mm":"flag_mm",
"mn":"flag_mn",
"mo":"flag_mo",
"mr":"flag_mr",
"ms":"flag_ms",
"mt":"flag_mt",
"mu":"flag_mu",
"mv":"flag_mv",
"mw":"flag_mw",
"mx":"flag_mx",
"my":"flag_my",
"mz":"flag_mz",
"na":"flag_na",
"nc":"flag_nc",
"ne":"flag_ne",
"nigeria":"flag_ng",
"ni":"flag_ni",
"nl":"flag_nl",
"no":"flag_no",
"np":"flag_np",
"nr":"flag_nr",
"nu":"flag_nu",
"nz":"flag_nz",
"om":"flag_om",
"pa":"flag_pa",
"pe":"flag_pe",
"pf":"flag_pf",
"pg":"flag_pg",
"ph":"flag_ph",
"pk":"flag_pk",
"pl":"flag_pl",
"pr":"flag_pr",
"ps":"flag_ps",
"pt":"flag_pt",
"pw":"flag_pw",
"py":"flag_py",
"qa":"flag_qa",
"ro":"flag_ro",
"rs":"flag_rs",
"ru":"flag_ru",
"rw":"flag_rw",
"saudiarabia":"flag_sa",
"saudi":"flag_sa",
"sb":"flag_sb",
"sc":"flag_sc",
"sd":"flag_sd",
"se":"flag_se",
"sg":"flag_sg",
"sh":"flag_sh",
"si":"flag_si",
"sk":"flag_sk",
"sl":"flag_sl",
"sm":"flag_sm",
"sn":"flag_sn",
"so":"flag_so",
"sr":"flag_sr",
"st":"flag_st",
"sv":"flag_sv",
"sy":"flag_sy",
"sz":"flag_sz",
"td":"flag_td",
"tg":"flag_tg",
"th":"flag_th",
"tj":"flag_tj",
"tl":"flag_tl",
"turkmenistan":"flag_tm",
"tn":"flag_tn",
"to":"flag_to",
"tr":"flag_tr",
"tt":"flag_tt",
"tuvalu":"flag_tv",
"tw":"flag_tw",
"tz":"flag_tz",
"ua":"flag_ua",
"ug":"flag_ug",
"us":"flag_us",
"uy":"flag_uy",
"uz":"flag_uz",
"va":"flag_va",
"vc":"flag_vc",
"ve":"flag_ve",
"vi":"flag_vi",
"vn":"flag_vn",
"vu":"flag_vu",
"wf":"flag_wf",
"waving_white_flag":"flag_white",
"ws":"flag_ws",
"xk":"flag_xk",
"ye":"flag_ye",
"za":"flag_za",
"zm":"flag_zm",
"zw":"flag_zw",
"clamshell_mobile_phone":"flip_phone",
"black_hard_shell_floppy_disk":"floppy_black",
"white_hard_shell_floppy_disk":"floppy_white",
"open_folder":"folder_open",
"fork_and_knife_with_plate":"fork_knife_plate",
"frame_with_picture":"frame_photo",
"frame_with_tiles":"frame_tiles",
"frame_with_an_x":"frame_x",
"anguished":"frowning",
"raised_hand_with_fingers_splayed":"hand_splayed",
"reversed_raised_hand_with_fingers_splayed":"hand_splayed_reverse",
"reversed_victory_hand":"hand_victory",
"heart_with_tip_on_the_left":"heart_tip",
"house_buildings":"homes",
"derelict_house_building":"house_abandoned",
"circled_information_source":"info",
"desert_island":"island",
"up_pointing_military_airplane":"jet_up",
"old_key":"key2",
"wired_keyboard":"keyboard",
"keyboard_and_mouse":"keyboard_mouse",
"musical_keyboard_with_jacks":"keyboard_with_jacks",
"couplekiss_mm":"kiss_mm",
"couplekiss_ww":"kiss_ww",
"satisfied":"laughing",
"left_hand_telephone_receiver":"left_receiver",
"man_in_business_suit_levitating":"levitate",
"weight_lifter":"lifter",
"light_mark":"light_check_mark",
"world_map":"map",
"sports_medal":"medal",
"studio_microphone":"microphone2",
"reversed_hand_with_middle_finger_extended":"middle_finger",
"lightning_mood_bubble":"mood_bubble_lightning",
"lightning_mood":"mood_lightning",
"racing_motorcycle":"motorcycle",
"snow_capped_mountain":"mountain_snow",
"one_button_mouse":"mouse_one",
"three_networked_computers":"network",
"rolled_up_newspaper":"newspaper2",
"note_page":"note",
"empty_note_page":"note_empty",
"note_pad":"notepad",
"empty_note_pad":"notepad_empty",
"spiral_note_pad":"notepad_spiral",
"oil_drum":"oil",
"grandma":"older_woman",
"optical_disc_icon":"optical_disk",
"lower_left_paintbrush":"paintbrush",
"linked_paperclips":"paperclips",
"national_park":"park",
"lower_left_ballpoint_pen":"pen_ballpoint",
"lower_left_fountain_pen":"pen_fountain",
"memo":"pencil",
"lower_left_pencil":"pencil3",
"black_pennant":"pennant_black",
"white_pennant":"pennant_white",
"no_piracy":"piracy",
"shit":"poop",
"hankey":"poop",
"poo":"poop",
"prohibited_sign":"prohibited",
"film_projector":"projector",
"racing_car":"race_car",
"railroad_track":"railway_track",
"right_speaker_with_one_sound_wave":"right_speaker_one",
"right_speaker_with_three_sound_waves":"right_speaker_three",
"skeleton":"skull",
"slightly_frowning_face":"slight_frown",
"slightly_smiling_face":"slight_smile",
"speaking_head_in_silhouette":"speaking_head",
"left_speech_bubble":"speech_left",
"right_speech_bubble":"speech_right",
"three_speech_bubbles":"speech_three",
"two_speech_bubbles":"speech_two",
"sleuth_or_spy":"spy",
"portable_stereo":"stereo",
"black_touchtone_telephone":"telephone_black",
"white_touchtone_telephone":"telephone_white",
"left_thought_bubble":"thought_left",
"right_thought_bubble":"thought_right",
"reversed_thumbs_down_sign":"thumbs_down_reverse",
"reversed_thumbs_up_sign":"thumbs_up_reverse",
"-1":"thumbsdown",
"+1":"thumbsup",
"admission_tickets":"tickets",
"hammer_and_wrench":"tools",
"diesel_locomotive":"train_diesel",
"triangle_with_rounded_corners":"triangle_round",
"turned_ok_hand_sign":"turned_ok_hand",
"raised_hand_with_part_between_middle_and_ring_fingers":"vulcan",
"left_writing_hand":"writing_hand"
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -7,7 +7,7 @@ module API
def commit_params(attrs)
{
file_path: attrs[:file_path],
current_branch: attrs[:branch_name],
source_branch: attrs[:branch_name],
target_branch: attrs[:branch_name],
commit_message: attrs[:commit_message],
file_content: attrs[:content],
......
......@@ -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
......
......@@ -8,14 +8,19 @@ module API
#
# Example Request:
# GET /users
# GET /users?search=Admin
# GET /users?username=root
get do
skip_ldap = params[:skip_ldap].present? && params[:skip_ldap] == 'true'
@users = User.all
@users = @users.active if params[:active].present?
@users = @users.non_ldap if skip_ldap
@users = @users.search(params[:search]) if params[:search].present?
@users = paginate @users
if params[:username].present?
@users = User.where(username: params[:username])
else
skip_ldap = params[:skip_ldap].present? && params[:skip_ldap] == 'true'
@users = User.all
@users = @users.active if params[:active].present?
@users = @users.non_ldap if skip_ldap
@users = @users.search(params[:search]) if params[:search].present?
@users = paginate @users
end
if current_user.is_admin?
present @users, with: Entities::UserFull
......
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",
hankey: "shit",
poop: "shit",
collision: "boom",
thumbsup: "+1",
thumbsdown: "-1",
punch: "facepunch",
raised_hand: "hand",
running: "runner",
ng_woman: "no_good",
shoe: "mans_shoe",
tshirt: "shirt",
honeybee: "bee",
flipper: "dolphin",
paw_prints: "feet",
waxing_gibbous_moon: "moon",
telephone: "phone",
knife: "hocho",
envelope: "email",
pencil: "memo",
open_book: "book",
sailboat: "boat",
red_car: "car",
lantern: "izakaya_lantern",
uk: "gb",
heavy_exclamation_mark: "exclamation",
squirrel: "shipit"
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.path_to_emoji_image(name)
"emoji/#{Emoji.emoji_filename(name)}.png"
def self.normilize_emoji_name(name)
aliases[name] || name
end
def self.normilize_emoji_name(name)
ALIASES[name] || name
def self.emoji_by_category
unless @emoji_by_category
@emoji_by_category = {}
emojis.each do |emoji_name, data|
data["name"] = emoji_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
def self.emojis
@emojis ||= begin
json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' )
JSON.parse(File.read(json_path))
end
end
def self.aliases
@aliases ||= begin
json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' )
JSON.parse(File.read(json_path))
end
end
end
......@@ -98,7 +98,7 @@ module Banzai
project = project_from_ref(project_ref)
if project && object = find_object(project, id)
title = escape_once(object_link_title(object))
title = object_link_title(object)
klass = reference_class(object_sym)
data = data_attribute(
......@@ -110,17 +110,11 @@ module Banzai
url = matches[:url] if matches.names.include?("url")
url ||= url_for_object(object, project)
text = link_text
unless text
text = object.reference_link_text(context[:project])
extras = object_link_text_extras(object, matches)
text += " (#{extras.join(", ")})" if extras.any?
end
text = link_text || object_link_text(object, matches)
%(<a href="#{url}" #{data}
title="#{title}"
class="#{klass}">#{text}</a>)
title="#{escape_once(title)}"
class="#{klass}">#{escape_once(text)}</a>)
else
match
end
......@@ -140,6 +134,15 @@ module Banzai
def object_link_title(object)
"#{object_class.name.titleize}: #{object.title}"
end
def object_link_text(object, matches)
text = object.reference_link_text(context[:project])
extras = object_link_text_extras(object, matches)
text += " (#{extras.join(", ")})" if extras.any?
text
end
end
end
end
......@@ -63,15 +63,15 @@ module Banzai
url = url_for_issue(id, project, only_path: context[:only_path])
title = escape_once("Issue in #{project.external_issue_tracker.title}")
title = "Issue in #{project.external_issue_tracker.title}"
klass = reference_class(:issue)
data = data_attribute(project: project.id, external_issue: id)
text = link_text || match
%(<a href="#{url}" #{data}
title="#{title}"
class="#{klass}">#{text}</a>)
title="#{escape_once(title)}"
class="#{klass}">#{escape_once(text)}</a>)
end
end
......
......@@ -60,7 +60,7 @@ module Banzai
text = link_text || render_colored_label(label)
%(<a href="#{url}" #{data}
class="#{klass}">#{text}</a>)
class="#{klass}">#{escape_once(text)}</a>)
else
match
end
......
......@@ -44,11 +44,11 @@ module Banzai
# Returns a String
def data_attribute(attributes = {})
attributes[:reference_filter] = self.class.name.demodulize
attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{value}") }.join(" ")
attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ")
end
def escape_once(html)
ERB::Util.html_escape_once(html)
html.html_safe? ? html : ERB::Util.html_escape_once(html)
end
def ignore_parents
......
......@@ -122,7 +122,7 @@ module Banzai
end
def link_tag(url, data, text)
%(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>)
%(<a href="#{url}" #{data} class="#{link_class}">#{escape_once(text)}</a>)
end
end
end
......
......@@ -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
......
......@@ -14,7 +14,7 @@ module Gitlab
# LDAP distinguished name is case-insensitive
identity = ::Identity.
where(provider: provider).
where('lower(extern_uid) = ?', uid.mb_chars.downcase.to_s).last
iwhere(extern_uid: uid).last
identity && identity.user
end
end
......@@ -33,7 +33,7 @@ module Gitlab
def find_by_uid_and_provider
self.class.find_by_uid_and_provider(
auth_hash.uid.downcase, auth_hash.provider)
auth_hash.uid, auth_hash.provider)
end
def find_by_email
......@@ -49,7 +49,7 @@ module Gitlab
# find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved.
identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider }
identity ||= gl_user.identities.build(provider: auth_hash.provider)
# For a new user set extern_uid to the LDAP DN
# For an existing user with matching email but changed DN, update the DN.
# For an existing user with no change in DN, this line changes nothing.
......
......@@ -64,7 +64,7 @@ module Gitlab
# If a corresponding person exists with same uid in a LDAP server,
# set up a Gitlab user with dual LDAP and Omniauth identities.
if user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn.downcase, ldap_person.provider)
if user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider)
# Case when a LDAP user already exists in Gitlab. Add the Omniauth identity to existing account.
user.identities.build(extern_uid: auth_hash.uid, provider: auth_hash.provider)
else
......
......@@ -51,6 +51,15 @@ module Gitlab
def allowed_fork_levels(origin_level)
[PRIVATE, INTERNAL, PUBLIC].select{ |level| level <= origin_level }
end
def level_name(level)
level_name = 'Unknown'
options.each do |name, lvl|
level_name = name if lvl == level.to_i
end
level_name
end
end
def private?
......
......@@ -92,7 +92,7 @@ check_pids(){
## Called when we have started the two processes and are waiting for their pid files.
wait_for_pids(){
# We are sleeping a bit here mostly because sidekiq is slow at writing it's pid
# We are sleeping a bit here mostly because sidekiq is slow at writing its pid
i=0;
while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; }; do
sleep 0.1;
......@@ -108,7 +108,7 @@ wait_for_pids(){
}
# We use the pids in so many parts of the script it makes sense to always check them.
# Only after start() is run should the pids change. Sidekiq sets it's own pid.
# Only after start() is run should the pids change. Sidekiq sets its own pid.
check_pids
......@@ -290,7 +290,7 @@ stop_gitlab() {
sleep 1
# Cleaning up unused pids
rm "$web_server_pid_path" 2>/dev/null
# rm "$sidekiq_pid_path" 2>/dev/null # Sidekiq seems to be cleaning up it's own pid.
# rm "$sidekiq_pid_path" 2>/dev/null # Sidekiq seems to be cleaning up its own pid.
rm -f "$gitlab_workhorse_pid_path"
if [ "$mail_room_enabled" = true ]; then
rm "$mail_room_pid_path" 2>/dev/null
......@@ -299,7 +299,7 @@ stop_gitlab() {
print_status
}
## Prints the status of GitLab and it's components.
## Prints the status of GitLab and its components.
print_status() {
check_status
if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
......@@ -333,7 +333,7 @@ print_status() {
fi
}
## Tells unicorn to reload it's config and Sidekiq to restart
## Tells unicorn to reload its config and Sidekiq to restart
reload_gitlab(){
exit_if_not_running
if [ "$wpid" = "0" ];then
......
......@@ -9,11 +9,11 @@ RAILS_ENV="production"
# The default is "git".
app_user="git"
# app_root defines the folder in which gitlab and it's components are installed.
# app_root defines the folder in which gitlab and its components are installed.
# The default is "/home/$app_user/gitlab"
app_root="/home/$app_user/gitlab"
# pid_path defines a folder in which the gitlab and it's components place their pids.
# pid_path defines a folder in which the gitlab and its components place their pids.
# This variable is also used below to define the relevant pids for the gitlab components.
# The default is "$app_root/tmp/pids"
pid_path="$app_root/tmp/pids"
......
......@@ -98,7 +98,7 @@ describe Projects::TreeController do
project_id: project.to_param,
id: 'master',
dir_name: path,
new_branch: target_branch,
target_branch: target_branch,
commit_message: 'Test commit message')
end
......@@ -108,8 +108,8 @@ describe Projects::TreeController do
it 'redirects to the new directory' do
expect(subject).
to redirect_to("/#{project.path_with_namespace}/blob/#{target_branch}/#{path}")
expect(flash[:notice]).to eq('The directory has been successfully created')
to redirect_to("/#{project.path_with_namespace}/tree/#{target_branch}/#{path}")
expect(flash[:notice]).to eq('The directory has been successfully created.')
end
end
......@@ -119,7 +119,7 @@ describe Projects::TreeController do
it 'does not allow overwriting of existing files' do
expect(subject).
to redirect_to("/#{project.path_with_namespace}/blob/master")
to redirect_to("/#{project.path_with_namespace}/tree/master")
expect(flash[:alert]).to eq('Directory already exists as a file')
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
......@@ -98,4 +98,56 @@ feature 'Login', feature: true do
expect(page).to have_content('Invalid login or password.')
end
end
describe 'with required two-factor authentication enabled' do
let(:user) { create(:user) }
before(:each) { stub_application_setting(require_two_factor_authentication: true) }
context 'with grace period defined' do
before(:each) do
stub_application_setting(two_factor_grace_period: 48)
login_with(user)
end
context 'within the grace period' do
it 'redirects to two-factor configuration page' do
expect(current_path).to eq new_profile_two_factor_auth_path
expect(page).to have_content('You must configure Two-Factor Authentication in your account until')
end
it 'two-factor configuration is skippable' do
expect(current_path).to eq new_profile_two_factor_auth_path
click_link 'Configure it later'
expect(current_path).to eq root_path
end
end
context 'after the grace period' do
let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) }
it 'redirects to two-factor configuration page' do
expect(current_path).to eq new_profile_two_factor_auth_path
expect(page).to have_content('You must configure Two-Factor Authentication in your account.')
end
it 'two-factor configuration is not skippable' do
expect(current_path).to eq new_profile_two_factor_auth_path
expect(page).not_to have_link('Configure it later')
end
end
end
context 'without grace pariod defined' do
before(:each) do
stub_application_setting(two_factor_grace_period: 0)
login_with(user)
end
it 'redirects to two-factor configuration page' do
expect(current_path).to eq new_profile_two_factor_auth_path
expect(page).to have_content('You must configure Two-Factor Authentication in your account.')
end
end
end
end
......@@ -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
......@@ -153,4 +141,11 @@ describe IssuesHelper do
expect(note_active_class(Note.all, @note.author)).to eq("active")
end
end
describe "#awards_sort" do
it "sorts a hash so thumbsup and thumbsdown are always on top" do
data = { "thumbsdown" => "some value", "lifter" => "some value", "thumbsup" => "some value" }
expect(awards_sort(data).keys).to eq(["thumbsup", "thumbsdown", "lifter"])
end
end
end
require 'rails_helper'
describe PageLayoutHelper do
describe 'page_description' do
it 'defaults to value returned by page_description_default helper' do
allow(helper).to receive(:page_description_default).and_return('Foo')
expect(helper.page_description).to eq 'Foo'
end
it 'returns the last-pushed description' do
helper.page_description('Foo')
helper.page_description('Bar')
helper.page_description('Baz')
expect(helper.page_description).to eq 'Baz'
end
it 'squishes multiple newlines' do
helper.page_description("Foo\nBar\nBaz")
expect(helper.page_description).to eq 'Foo Bar Baz'
end
it 'truncates' do
helper.page_description <<-LOREM.strip_heredoc
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo
ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis
dis parturient montes, nascetur ridiculus mus. Donec quam felis,
ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa
quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget,
arcu.
LOREM
expect(helper.page_description).to end_with 'quam felis,...'
end
it 'sanitizes all HTML' do
helper.page_description("<b>Bold</b> <h1>Header</h1>")
expect(helper.page_description).to eq 'Bold Header'
end
end
describe 'page_description_default' do
it 'uses Project description when available' do
project = double(description: 'Project Description')
helper.instance_variable_set(:@project, project)
expect(helper.page_description_default).to eq 'Project Description'
end
it 'uses brand_title when Project description is nil' do
project = double(description: nil)
helper.instance_variable_set(:@project, project)
expect(helper).to receive(:brand_title).and_return('Brand Title')
expect(helper.page_description_default).to eq 'Brand Title'
end
it 'falls back to brand_title' do
allow(helper).to receive(:brand_title).and_return('Brand Title')
expect(helper.page_description_default).to eq 'Brand Title'
end
end
describe 'page_image' do
it 'defaults to the GitLab logo' do
expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
end
context 'with @project' do
it 'uses Project avatar if available' do
project = double(avatar_url: 'http://example.com/uploads/avatar.png')
helper.instance_variable_set(:@project, project)
expect(helper.page_image).to eq project.avatar_url
end
it 'falls back to the default' do
project = double(avatar_url: nil)
helper.instance_variable_set(:@project, project)
expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
end
end
context 'with @user' do
it 'delegates to avatar_icon helper' do
user = double('User')
helper.instance_variable_set(:@user, user)
expect(helper).to receive(:avatar_icon).with(user)
helper.page_image
end
end
end
describe 'page_card_attributes' do
it 'raises ArgumentError when given more than two attributes' do
map = { foo: 'foo', bar: 'bar', baz: 'baz' }
expect { helper.page_card_attributes(map) }.
to raise_error(ArgumentError, /more than two attributes/)
end
it 'rejects blank values' do
map = { foo: 'foo', bar: '' }
helper.page_card_attributes(map)
expect(helper.page_card_attributes).to eq({ foo: 'foo' })
end
end
describe 'page_card_meta_tags' do
it 'returns the twitter:label and twitter:data tags' do
allow(helper).to receive(:page_card_attributes).and_return(foo: 'bar')
tags = helper.page_card_meta_tags
aggregate_failures do
expect(tags).to include %q(<meta property="twitter:label1" content="foo" />)
expect(tags).to include %q(<meta property="twitter:data1" content="bar" />)
end
end
end
end
%form.js-create-branch-form
%input.js-branch-name
.js-branch-name-error
%input{id: "ref"}
#= require jquery-ui
#= require new_branch_form
describe 'Branch', ->
describe 'create a new branch', ->
fixture.preload('new_branch.html')
fillNameWith = (value) ->
$('.js-branch-name').val(value).trigger('blur')
expectToHaveError = (error) ->
expect($('.js-branch-name-error span').text()).toEqual(error)
beforeEach ->
fixture.load('new_branch.html')
$('form').on 'submit', (e) -> e.preventDefault()
@form = new NewBranchForm($('.js-create-branch-form'), [])
it "can't start with a dot", ->
fillNameWith '.foo'
expectToHaveError "can't start with '.'"
it "can't start with a slash", ->
fillNameWith '/foo'
expectToHaveError "can't start with '/'"
it "can't have two consecutive dots", ->
fillNameWith 'foo..bar'
expectToHaveError "can't contain '..'"
it "can't have spaces anywhere", ->
fillNameWith ' foo'
expectToHaveError "can't contain spaces"
fillNameWith 'foo bar'
expectToHaveError "can't contain spaces"
fillNameWith 'foo '
expectToHaveError "can't contain spaces"
it "can't have ~ anywhere", ->
fillNameWith '~foo'
expectToHaveError "can't contain '~'"
fillNameWith 'foo~bar'
expectToHaveError "can't contain '~'"
fillNameWith 'foo~'
expectToHaveError "can't contain '~'"
it "can't have tilde anwhere", ->
fillNameWith '~foo'
expectToHaveError "can't contain '~'"
fillNameWith 'foo~bar'
expectToHaveError "can't contain '~'"
fillNameWith 'foo~'
expectToHaveError "can't contain '~'"
it "can't have caret anywhere", ->
fillNameWith '^foo'
expectToHaveError "can't contain '^'"
fillNameWith 'foo^bar'
expectToHaveError "can't contain '^'"
fillNameWith 'foo^'
expectToHaveError "can't contain '^'"
it "can't have : anywhere", ->
fillNameWith ':foo'
expectToHaveError "can't contain ':'"
fillNameWith 'foo:bar'
expectToHaveError "can't contain ':'"
fillNameWith ':foo'
expectToHaveError "can't contain ':'"
it "can't have question mark anywhere", ->
fillNameWith '?foo'
expectToHaveError "can't contain '?'"
fillNameWith 'foo?bar'
expectToHaveError "can't contain '?'"
fillNameWith 'foo?'
expectToHaveError "can't contain '?'"
it "can't have asterisk anywhere", ->
fillNameWith '*foo'
expectToHaveError "can't contain '*'"
fillNameWith 'foo*bar'
expectToHaveError "can't contain '*'"
fillNameWith 'foo*'
expectToHaveError "can't contain '*'"
it "can't have open bracket anywhere", ->
fillNameWith '[foo'
expectToHaveError "can't contain '['"
fillNameWith 'foo[bar'
expectToHaveError "can't contain '['"
fillNameWith 'foo['
expectToHaveError "can't contain '['"
it "can't have a backslash anywhere", ->
fillNameWith '\\foo'
expectToHaveError "can't contain '\\'"
fillNameWith 'foo\\bar'
expectToHaveError "can't contain '\\'"
fillNameWith 'foo\\'
expectToHaveError "can't contain '\\'"
it "can't contain a sequence @{ anywhere", ->
fillNameWith '@{foo'
expectToHaveError "can't contain '@{'"
fillNameWith 'foo@{bar'
expectToHaveError "can't contain '@{'"
fillNameWith 'foo@{'
expectToHaveError "can't contain '@{'"
it "can't have consecutive slashes", ->
fillNameWith 'foo//bar'
expectToHaveError "can't contain consecutive slashes"
it "can't end with a slash", ->
fillNameWith 'foo/'
expectToHaveError "can't end in '/'"
it "can't end with a dot", ->
fillNameWith 'foo.'
expectToHaveError "can't end in '.'"
it "can't end with .lock", ->
fillNameWith 'foo.lock'
expectToHaveError "can't end in '.lock'"
it "can't be the single character @", ->
fillNameWith '@'
expectToHaveError "can't be '@'"
it "concatenates all error messages", ->
fillNameWith '/foo bar?~.'
expectToHaveError "can't start with '/', can't contain spaces, '?', '~', can't end in '.'"
it "doesn't duplicate error messages", ->
fillNameWith '?foo?bar?zoo?'
expectToHaveError "can't contain '?'"
it "removes the error message when is a valid name", ->
fillNameWith 'foo?bar'
expect($('.js-branch-name-error span').length).toEqual(1)
fillNameWith 'foobar'
expect($('.js-branch-name-error span').length).toEqual(0)
it "can have dashes anywhere", ->
fillNameWith '-foo-bar-zoo-'
expect($('.js-branch-name-error span').length).toEqual(0)
it "can have underscores anywhere", ->
fillNameWith '_foo_bar_zoo_'
expect($('.js-branch-name-error span').length).toEqual(0)
it "can have numbers anywhere", ->
fillNameWith '1foo2bar3zoo4'
expect($('.js-branch-name-error span').length).toEqual(0)
it "can be only letters", ->
fillNameWith 'foo'
expect($('.js-branch-name-error span').length).toEqual(0)
......@@ -42,6 +42,21 @@ describe Gitlab::LDAP::User, lib: true do
end
end
describe '.find_by_uid_and_provider' do
it 'retrieves the correct user' do
special_info = {
name: 'John Åström',
email: 'john@example.com',
nickname: 'jastrom'
}
special_hash = OmniAuth::AuthHash.new(uid: 'CN=John Åström,CN=Users,DC=Example,DC=com', provider: 'ldapmain', info: special_info)
special_chars_user = described_class.new(special_hash)
user = special_chars_user.save
expect(described_class.find_by_uid_and_provider(special_hash.uid, special_hash.provider)).to eq user
end
end
describe :find_or_create do
it "finds the user if already existing" do
create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain')
......
......@@ -27,6 +27,7 @@
# admin_notification_email :string(255)
# shared_runners_enabled :boolean default(TRUE), not null
# max_artifacts_size :integer default(100), not null
# runners_registration_token :string(255)
#
require 'spec_helper'
......
......@@ -81,4 +81,22 @@ describe Issue, "Issuable" do
expect(hook_data[:object_attributes]).to eq(issue.hook_attrs)
end
end
describe '#card_attributes' do
it 'includes the author name' do
allow(issue).to receive(:author).and_return(double(name: 'Robert'))
allow(issue).to receive(:assignee).and_return(nil)
expect(issue.card_attributes).
to eq({ 'Author' => 'Robert', 'Assignee' => nil })
end
it 'includes the assignee name' do
allow(issue).to receive(:author).and_return(double(name: 'Robert'))
allow(issue).to receive(:assignee).and_return(double(name: 'Douwe'))
expect(issue.card_attributes).
to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
end
end
end
......@@ -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
......
......@@ -62,4 +62,14 @@ describe GlobalMilestone, models: true do
expect(@global_milestone.milestones.count).to eq(3)
end
end
describe :safe_title do
let(:milestone) { create(:milestone, title: "git / test", project: project1) }
it 'should strip out slashes and spaces' do
global_milestone = GlobalMilestone.new(milestone.title, [milestone])
expect(global_milestone.safe_title).to eq('git-test')
end
end
end
......@@ -137,9 +137,14 @@ describe Note, models: true do
create :note, note: "smile", is_award: true
end
it "returns grouped array of notes" do
expect(Note.grouped_awards.first.first).to eq("smile")
expect(Note.grouped_awards.first.last).to match_array(Note.all)
it "returns grouped hash of notes" do
expect(Note.grouped_awards.keys.size).to eq(3)
expect(Note.grouped_awards["smile"]).to match_array(Note.all)
end
it "returns thumbsup and thumbsdown always" do
expect(Note.grouped_awards["thumbsup"]).to match_array(Note.none)
expect(Note.grouped_awards["thumbsdown"]).to match_array(Note.none)
end
end
......@@ -164,8 +169,8 @@ describe Note, models: true do
let(:issue) { create :issue }
it "converts aliases to actual name" do
note = create :note, note: ":thumbsup:", noteable: issue
expect(note.reload.note).to eq("+1")
note = create :note, note: ":+1:", noteable: issue
expect(note.reload.note).to eq("thumbsup")
end
end
end
......@@ -590,4 +590,28 @@ describe Project, models: true do
end
end
end
describe '#visibility_level_allowed?' do
let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL }
context 'when checking on non-forked project' do
it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy }
it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy }
it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PUBLIC)).to be_truthy }
end
context 'when checking on forked project' do
let(:forked_project) { create :forked_project_with_submodules }
before do
forked_project.build_forked_project_link(forked_to_project_id: forked_project.id, forked_from_project_id: project.id)
forked_project.save
end
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy }
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy }
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PUBLIC)).to be_falsey }
end
end
end
......@@ -118,7 +118,7 @@ describe API::API, api: true do
branch_name: 'new design',
ref: branch_sha
expect(response.status).to eq(400)
expect(json_response['message']).to eq('Branch name invalid')
expect(json_response['message']).to eq('Branch name is invalid')
end
it 'should return 400 if branch already exists' do
......
......@@ -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]
......
......@@ -27,6 +27,13 @@ describe API::API, api: true do
user['username'] == username
end['username']).to eq(username)
end
it "should return one user" do
get api("/users?username=#{omniauth_user.username}", user)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.first['username']).to eq(omniauth_user.username)
end
end
context "when admin" do
......
......@@ -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
......
......@@ -100,6 +100,45 @@ describe Projects::UpdateService, services: true do
end
end
describe :visibility_level do
let(:user) { create :user, admin: true }
let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL }
let(:forked_project) { create :forked_project_with_submodules, visibility_level: Gitlab::VisibilityLevel::INTERNAL }
let(:opts) { {} }
before do
forked_project.build_forked_project_link(forked_to_project_id: forked_project.id, forked_from_project_id: project.id)
forked_project.save
@created_internal = project.internal?
@fork_created_internal = forked_project.internal?
end
context 'should update forks visibility level when parent set to more restrictive' do
before do
opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
update_project(project, user, opts).inspect
end
it { expect(@created_internal).to be_truthy }
it { expect(@fork_created_internal).to be_truthy }
it { expect(project.private?).to be_truthy }
it { expect(project.forks.first.private?).to be_truthy }
end
context 'should not update forks visibility level when parent set to less restrictive' do
before do
opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
update_project(project, user, opts).inspect
end
it { expect(@created_internal).to be_truthy }
it { expect(@fork_created_internal).to be_truthy }
it { expect(project.public?).to be_truthy }
it { expect(project.forks.first.internal?).to be_truthy }
end
end
def update_project(project, user, opts)
Projects::UpdateService.new(project, user, opts).execute
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